C/С++ перечисления: обнаружение, когда несколько элементов сопоставляются с одним и тем же значением
Есть ли способ компиляции для обнаружения/предотвращения дублирования значений в перечислении C/С++?
Ловушка заключается в том, что имеется несколько элементов, которые инициализируются явными значениями.
Фон:
Я унаследовал некоторый код C, например:
#define BASE1_VAL (5)
#define BASE2_VAL (7)
typedef enum
{
MsgFoo1A = BASE1_VAL, // 5
MsgFoo1B, // 6
MsgFoo1C, // 7
MsgFoo1D, // 8
MsgFoo1E, // 9
MsgFoo2A = BASE2_VAL, // Uh oh! 7 again...
MsgFoo2B // Uh oh! 8 again...
} FOO;
Проблема заключается в том, что по мере роста кода и по мере того, как разработчики добавляют больше сообщений в группу MsgFoo1x
, в конце концов он переполняет BASE2_VAL
.
Этот код в конечном итоге будет перенесен на С++, поэтому, если есть только С++-решение (магия шаблона?), это нормально - но решение, которое работает с C и С++, лучше.
Ответы
Ответ 1
Есть несколько способов проверить это время компиляции, но они могут не всегда работать для вас. Начните с вставки значения маркера "маркер" прямо перед MsgFoo2A.
typedef enum
{
MsgFoo1A = BASE1_VAL,
MsgFoo1B,
MsgFoo1C,
MsgFoo1D,
MsgFoo1E,
MARKER_1_DONT_USE, /* Don't use this value, but leave it here. */
MsgFoo2A = BASE2_VAL,
MsgFoo2B
} FOO;
Теперь нам нужен способ обеспечить MARKER_1_DONT_USE < BASE2_VAL
во время компиляции. Существует два распространенных метода.
Массивы отрицательного размера
Ошибка объявления массива с отрицательным размером. Это выглядит немного уродливым, но оно работает.
extern int IGNORE_ENUM_CHECK[MARKER_1_DONT_USE > BASE2_VAL ? -1 : 1];
Почти каждый компилятор, когда-либо написанный, генерирует ошибку, если MARKER_1_DONT_USE больше, чем BASE_2_VAL. GCC выплевывает:
test.c:16: error: size of array ‘IGNORE_ENUM_CHECK’ is negative
Статические утверждения
Если ваш компилятор поддерживает C11, вы можете использовать _Static_assert
. Поддержка C11 не является повсеместной, но ваш компилятор может поддерживать _Static_assert
в любом случае, тем более, что соответствующая функция на С++ широко поддерживается.
_Static_assert(MARKER_1_DONT_USE < BASE2_VAL, "Enum values overlap.");
GCC выдает следующее сообщение:
test.c:16:1: error: static assertion failed: "Enum values overlap."
_Static_assert(MARKER_1_DONT_USE < BASE2_VAL, "Enum values overlap.");
^
Ответ 2
Я не видел "хороших" в ваших требованиях, поэтому я представляю это решение, реализованное с использованием библиотеки Boost Preprocessor.
Как предварительный отказ от ответственности, я не использовал Boost.Preprocessor много, и я только тестировал это с представленными здесь тестовыми примерами, поэтому могут быть ошибки, и может быть более простой и понятный способ сделать это. Я, конечно, приветствую комментарии, исправления, предложения, оскорбления и т.д.
Здесь мы идем:
#include <boost/preprocessor.hpp>
#define EXPAND_ENUM_VALUE(r, data, i, elem) \
BOOST_PP_SEQ_ELEM(0, elem) \
BOOST_PP_IIF( \
BOOST_PP_EQUAL(BOOST_PP_SEQ_SIZE(elem), 2), \
= BOOST_PP_SEQ_ELEM(1, elem), \
BOOST_PP_EMPTY()) \
BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(data, BOOST_PP_ADD(i, 1)))
#define ADD_CASE_FOR_ENUM_VALUE(r, data, elem) \
case BOOST_PP_SEQ_ELEM(0, elem) : break;
#define DEFINE_UNIQUE_ENUM(name, values) \
enum name \
{ \
BOOST_PP_SEQ_FOR_EACH_I(EXPAND_ENUM_VALUE, \
BOOST_PP_SEQ_SIZE(values), values) \
}; \
\
namespace detail \
{ \
void UniqueEnumSanityCheck##name() \
{ \
switch (name()) \
{ \
BOOST_PP_SEQ_FOR_EACH(ADD_CASE_FOR_ENUM_VALUE, name, values) \
} \
} \
}
Затем мы можем использовать его так:
DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday) (1))
((Tuesday) (2))
((Wednesday) )
((Thursday) (4)))
Значение перечислителя является необязательным; этот код генерирует нумерацию, эквивалентную:
enum DayOfWeek
{
Monday = 1,
Tuesday = 2,
Wednesday,
Thursday = 4
};
Он также генерирует функцию проверки работоспособности, которая содержит оператор switch, как описано в ответе Бен Вейгта. Если мы изменим объявление перечисления таким образом, что у нас есть уникальные значения перечислителя, например,
DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday) (1))
((Tuesday) (2))
((Wednesday) )
((Thursday) (1)))
он не будет компилироваться (Visual С++ сообщает ожидаемую ошибку C2196: значение case '1' уже используется).
Спасибо также Matthieu M., чей ответ на другой вопрос заинтересовал меня в библиотеке Preprocessor Boost.
Ответ 3
Я не верю, что есть способ обнаружить это с самим языком, учитывая, что существуют мыслимые случаи, когда вы хотите, чтобы два значения перечисления были одинаковыми. Тем не менее, вы всегда можете убедиться, что все явно заданные элементы находятся в верхней части списка:
typedef enum
{
MsgFoo1A = BASE1_VAL, // 5
MsgFoo2A = BASE2_VAL, // 7
MsgFoo1B, // 8
MsgFoo1C, // 9
MsgFoo1D, // 10
MsgFoo1E, // 11
MsgFoo2B // 12
} FOO;
Пока назначенные значения находятся наверху, конфликт не возможен, если по какой-то причине макросы расширяются до значений, которые являются одинаковыми.
Обычно эта проблема преодолевается путем предоставления фиксированного количества бит для каждой группы MsgFooX и обеспечения того, чтобы каждая группа не переполняла, она выделяла количество бит. Решение "Количество бит" хорошо, потому что позволяет поразрядному тестированию определить, к какой группе сообщений что-то принадлежит. Но здесь нет встроенной функции языка, потому что есть законные случаи для перечисления, имеющие два одинаковых значения:
typedef enum
{
gray = 4, //Gr[ae]y should be the same
grey = 4,
color = 5, //Also makes sense in some cases
couleur = 5
} FOO;
Ответ 4
Я ничего не знаю о том, что будет автоматически проверять всех членов перечисления, но если вы хотите проверить, что будущие изменения инициализаторов (или макросов, на которые они полагаются) не вызывают конфликтов:
switch (0) {
case MsgFoo1A: break;
case MsgFoo1B: break;
case MsgFoo1C: break;
case MsgFoo1D: break;
case MsgFoo1E: break;
case MsgFoo2A: break;
case MsgFoo2B: break;
}
приведет к ошибке компилятора, если какое-либо из интегральных значений будет повторно использовано, и большинство компиляторов даже скажут вам, какое значение (числовое значение) было проблемой.
Ответ 5
Вы можете перевернуть более надежное решение для определения перечислений с помощью Boost.Preprocessor - тем более, что время - это другое дело.
Если вы все равно переходите на С++, возможно (предложенный) Boost.Enum подходит вам (доступно через Boost Vault).
Другим подходом может быть использование чего-то типа gccxml (или более комфортно pygccxml), чтобы идентифицировать кандидатов для ручного осмотра.
Ответ 6
В то время как у нас нет полной рефлексии, вы можете решить эту проблему, если вы можете восстановить значения перечисления.
Где-то это объявлено:
enum E { A = 0, B = 0 };
в другом месте мы строим эту технику:
template<typename S, S s0, S... s>
struct first_not_same_as_rest : std::true_type {};
template<typename S, S s0, S s1, S... s>
struct first_not_same_as_rest : std::integral_constant< bool,
(s0 != s1) && first_not_same_as_rest< S, s0, s... >::value
> {};
template<typename S, S... s>
struct is_distinct : std::true_type {};
template<typename S, S s0, S... s>
struct is_distinct : std::integral_constant< bool,
std::is_distinct<S, s...>::value &&
first_not_same_as_rest< S, s0, s... >::value
> {};
Как только у вас есть этот механизм (для которого требуется С++ 11), мы можем сделать следующее:
static_assert( is_distinct< E, A, B >::value, "duplicate values in E detected" );
и во время компиляции мы гарантируем, что два элемента не равны.
Это требует O (n) глубины рекурсии, а O (n ^ 2) работает компилятором во время компиляции, поэтому для чрезвычайно больших перечислений это может вызвать проблемы. Глубина O (lg (n)) и O (n lg (n)) работают с гораздо большим постоянным коэффициентом, можно сделать, сначала отсортировав список элементов, но это намного больше работает.
С кодом отражения enum, предложенным для С++ 1y-С++ 17, это будет выполнимо без повторения элементов.
Ответ 7
Мне не понравились все ответы, уже опубликованные здесь, но они дали мне несколько идей. Важнейшим методом является использование ответа Ben Voight на использование оператора switch. Если несколько случаев в коммутаторе имеют один и тот же номер, вы получите ошибку компиляции.
Наиболее полезно как для себя, так и, возможно, для оригинального плаката, это не требует каких-либо возможностей С++.
Чтобы очистить все, я использовал ответ aaronps на Как избежать повторения себя при создании перечня С++ и зависимой структуры данных?
Сначала определите это в некотором заголовке где-нибудь:
#define DEFINE_ENUM_VALUE(name, value) name=value,
#define CHECK_ENUM_VALUE(name, value) case name:
#define DEFINE_ENUM(enum_name, enum_values) \
typedef enum { enum_values(DEFINE_ENUM_VALUE) } enum_name;
#define CHECK_ENUM(enum_name, enum_values) \
void enum_name ## _test (void) { switch(0) { enum_values(CHECK_ENUM_VALUE); } }
Теперь, когда вам нужно иметь перечисление:
#define COLOR_VALUES(GEN) \
GEN(Red, 1) \
GEN(Green, 2) \
GEN(Blue, 2)
Наконец, эти строки необходимы для фактического перечисления:
DEFINE_ENUM(Color, COLOR_VALUES)
CHECK_ENUM(Color, COLOR_VALUES)
DEFINE_ENUM
создает тип данных перечисления. CHECK_ENUM
выполняет функцию тестирования, которая включает все значения перечисления. Компилятор будет сбой при компиляции CHECK_ENUM
, если у вас есть дубликаты.