Существует ли время компиляции, способ обнаружить / предотвращает дублирующиеся значения в рамках перечисления 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++, поэтому если будет C ++-only решение (обработайте волшебство по шаблону?), это в порядке - но решение, которое работает с C и C++, лучше.
Есть несколько способов проверить это время компиляции, но они не всегда могут сработать для вас. Начните с вставки значения перечисления «маркер» прямо перед 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
Объявление массива отрицательного размера является ошибкой. Это выглядит немного некрасиво, но работает.
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
в любом случае, тем более что соответствующая функция в C ++ широко поддерживается.
_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.");
^
Я не верю, что есть способ обнаружить это с помощью самого языка, учитывая возможные случаи, когда вы хотите, чтобы два значения перечисления были одинаковыми. Однако вы всегда можете убедиться, что все явно заданные элементы находятся наверху списка:
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;
Я не знаю ничего, что автоматически проверяло бы все члены перечисления, но если вы хотите проверить будущие изменения инициализаторов (или макросов, которые они полагаться на) не вызывают коллизий:
switch (0) {
case MsgFoo1A: break;
case MsgFoo1B: break;
case MsgFoo1C: break;
case MsgFoo1D: break;
case MsgFoo1E: break;
case MsgFoo2A: break;
case MsgFoo2B: break;
}
вызовет ошибку компилятора, если любое из целочисленных значений будет повторно использовано, и большинство компиляторов даже скажут вам, какое значение (числовое значение) было проблемой.
Вы можете создать более надежное решение для определения перечислений с помощью Boost.Preprocessor - другое дело, стоит ли потратить время.
Если вы все равно переходите на C ++, возможно, вам подходит (предлагаемый) Boost.Enum (доступен через Boost Vault ).
Другой подход может заключаться в использовании чего-то вроде gccxml (или, что более удобно, pygccxml ) для определения кандидатов для проверки вручную.
Я не увидел в ваших требованиях слова "красивый", поэтому предлагаю следующее решение, реализованное с помощью библиотеки 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, как описано в ответе Ben Voigt. Если мы изменим объявление перечисления таким образом, что у нас будут неуникальные значения перечислителя, например,
DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday) (1))
((Tuesday) (2))
((Wednesday) )
((Thursday) (1)))
это не скомпилируется (Visual C++ сообщает ожидаемую ошибку C2196: case value '1' already used).
Спасибо также Matthieu M., чей ответ на другой вопрос заинтересовал меня библиотекой Boost Preprocessor.