У нас есть клиент-серверное приложение, в котором старые серверы поддерживаются новыми клиентами. Оба поддерживают общий набор иконок.
Мы представляем значки как неявное перечисление, которое включено в сборки сервера и клиента:
enum icons_t { // rev 1.0
ICON_A, // 0
ICON_B, // 1
ICON_C // 2
};
Иногда мы удаляем значки (не использовались или использовались внутренне и не были перечислены в нашем API), что приводило к фиксации следующего кода:
enum icons_t { // rev 2.0
ICON_B, // 0
ICON_C // 1 (now if a rev 1.0 server uses ICON_B, it will get ICON_C instead)
};
Я изменил наше перечисление на следующее, чтобы попытаться обойти это:
// Big scary header about commenting out old icons
enum icons_t { // rev 2.1
// Removed: ICON_A = 0,
ICON_B = 1,
ICON_C = 2
};
Теперь меня беспокоит плохое слияние, когда несколько людей добавляют новые значки:
// Big scary header about commenting out old icons
enum icons_t { // rev 30
// Removed: ICON_A = 0,
ICON_B = 1,
ICON_C = 2,
ICON_D = 3,
ICON_E = 3 // Bad merge leaves 2 icons with same value
};
Поскольку это перечисление, у нас нет способа утверждать, что значения не уникальны.
Есть ли лучшая структура данных для управления этими данными или изменение дизайна, которое не будет подвержено подобным ошибкам? Мои мысли были направлены на инструмент для анализа запросов извлечения и блокировки слияний, если эта проблема обнаружена.
Ранее я делал тесты, которые проверяют предыдущие сборки и сканируют заголовочные файлы на предмет такого типа нарушения работы версий. Вы можете использовать diff для генерации отчета о любых изменениях, grep для общего шаблона и определить разницу между удалением записи с фиксированным индексом, изменением индекса записи и удалением или вставкой плавающей записи индекса.
Единственный очевидный способ избежать этого — НЕ удалять мертвые индексы, а переименовывать их, т.е. ICON_A
становится ICON_A_RETIRED
и его слот зарезервирован навсегда. Тем не менее, неизбежно, что кто-то изменит индекс, поэтому хороший тестовый модуль также поможет. Форсировать шаблонный стиль означает, что тест проще, чем справиться с общим случаем.
Другой прием может заключаться в том, чтобы признать, что проблема возникнет, но если это проблема только для клиентов, и при каждом выпуске / пересмотре программного обеспечения обновляйте базовый номер для диапазона, выпускайте программное обеспечение и обновляйте снова, чтобы версия разработчика была никогда не совместимы с выпуском, например
#define ICON_RANGE 0x1000
#define ICON_REVISION_BASE ((RELEASENUM+ISDEVFLAG)*ICON_RANGE)
enum icon_t {
iconTMax = ICON_REVISION_BASE+ICON_RANGE,
iconTBase = ICON_REVISION_BASE,
icon_A,
icon_B,
Затем во время выполнения любые значки, отсутствующие в текущем диапазоне, легко отклоняются, или вы можете обеспечить специальный поиск между версиями, возможно, сгенерированный путем обхода ваших версий контроля версий. Обратите внимание, что таким способом вы можете обеспечить обратную совместимость, но не прямую совместимость. Более новый код превентивно выполняет обратный перевод номеров своих значков для отправки в более старые модули, что может потребовать больше усилий, чем оно того стоит.
Эта мысль только что пришла мне в голову: если мы сохраняем литерал в конце для размера перечисления, наши модульные тесты могут использовать это для подтверждения, если мы не проверили каждый литерал перечисления:
enum icons_t {
ICON_A_DEPRECATED,
ICON_B,
ICON_C,
ICON_COUNT // ALWAYS KEEP THIS LAST
};
Тогда в тестировании:
unsigned int verifyCount = 0;
verify(0, ICON_A_DEPRECATED); // verifyCount++, assert 0 was not verified before
verify(1, ICON_B); // verifyCount++, assert 1 was never verified before
assert(ICON_COUNT == verifyCount, "Not all icons verified");
Тогда наша единственная проблема — обеспечить прохождение тестов перед выпуском, что мы должны делать в любом случае.
Так как вопрос был помечен C ++ 11, это может быть лучше обработано с Перечисленные области.
Читайте об этом здесь: http://en.cppreference.com/w/cpp/language/enum
Поскольку один и тот же файл enum включен как в клиент, так и в сервер, то удаление любой записи приведет к сбою компиляции в тех местах, где используется отсутствующая запись.
Все, что нужно изменить, это ваш icon_t
,
Обновите его с enum
в enum class
enum class icon_t
{
ICON_A,
ICON_B,
};
Теперь вы не можете нагло пройти int
вместо icon_t
, Это уменьшает вашу вероятность совершать ошибки радикально.
Так что вызывающая сторона
#include <iostream>
enum class icon_t
{
ICON_A,
ICON_B,
};
void test_icon(icon_t const & icon)
{
if (icon == icon_t::ICON_A)
std::cout << "icon_t::ICON_A";
if (icon == icon_t::ICON_B)
std::cout << "icon_t::ICON_B";
}
int main()
{
auto icon = icon_t::ICON_A;
test_icon(icon); // this is ok
test_icon(1); // Fails at compile time : no known conversion from 'int' to 'const icon_t' for 1st argument
return 0;
}
Кроме того, извлечение числовых значений разрешено из перечислителей Scoped. static_cast
в int
позволено. Если необходимо.
int n = static_cast<int>(icon); // Would return 0, the index of icon_t::ICON_A