Перечисления C / C ++: обнаружение, когда несколько элементов отображаются на одно и то же значение - PullRequest
22 голосов
/ 05 апреля 2010

Существует ли способ компиляции во время компиляции для обнаружения / предотвращения дублирования значений в перечислении 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 ++ (магия шаблона?), Ничего страшного, но решение, которое работает с C и C ++, лучше.

Ответы [ 7 ]

14 голосов
/ 05 апреля 2010

Есть несколько способов проверить время компиляции, но они могут не всегда работать на вас. Начните с вставки значения перечисления "marker" прямо перед 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 в любом случае, тем более что соответствующая функция в 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.");
 ^
7 голосов
/ 05 апреля 2010

Я не вижу "симпатичных" в ваших требованиях, поэтому я отправляю это решение, реализованное с использованием библиотеки препроцессора Boost.

Как предварительный отказ от ответственности, я не много использовал 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
};

Он также генерирует функцию проверки работоспособности, которая содержит оператор переключения, как описано в ответ Бена Фойгта . Если мы изменим объявление перечисления так, чтобы у нас были неуникальные значения перечислителя, например,

DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday)    (1))
                              ((Tuesday)   (2))
                              ((Wednesday)    )
                              ((Thursday)  (1)))

не будет компилироваться (Visual C ++ сообщает об ожидаемой ошибке C2196: значение регистра '1' уже используется ).

Спасибо также Мэтью М., , чей ответ на другой вопрос заинтересовал меня библиотекой препроцессора Boost.

3 голосов
/ 05 апреля 2010

Я не знаю ничего, что автоматически проверяло бы все члены перечисления, но если вы хотите проверить, что будущие изменения инициализаторов (или макросов, на которые они полагаются) не вызовут коллизий:

switch (0) {
    case MsgFoo1A: break;
    case MsgFoo1B: break;
    case MsgFoo1C: break;
    case MsgFoo1D: break;
    case MsgFoo1E: break;
    case MsgFoo2A: break;
    case MsgFoo2B: break;
}

вызовет ошибку компилятора, если какое-либо из целочисленных значений будет использовано повторно, и большинство компиляторов даже скажут вам, какое значение (числовое значение) было проблемой.

3 голосов
/ 05 апреля 2010

Я не верю, что есть способ обнаружить это с помощью самого языка, учитывая, что возможны случаи, когда вы хотите, чтобы два значения перечисления были одинаковыми. Однако вы всегда можете убедиться, что все явно заданные элементы находятся вверху списка:

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;
1 голос
/ 13 января 2014

Хотя у нас нет полной рефлексии, вы можете решить эту проблему, если сможете использовать значения перечисления.

Где-то это объявлено:

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
> {};

Если у вас есть этот механизм (для которого требуется C ++ 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, предложенного для C ++ 1y-C ++ 17, это будет выполнимо без учета элементов.

1 голос
/ 05 апреля 2010

Вы можете создать более надежное решение для определения перечислений, используя Boost.Preprocessor - не стоит ли тратить время на это другое дело.

Если вы все равно переходите на C ++, возможно, (предложенный) Boost.Enum вам подходит (доступно через Boost Vault ).

Другой подход может заключаться в том, чтобы использовать что-то вроде gccxml (или более удобно pygccxml ) для идентификации кандидатов для ручной проверки.

0 голосов
/ 20 марта 2014

Мне не понравился ни один из ответов, уже опубликованных здесь, но они дали мне некоторые идеи. Важнейший метод - полагаться на ответ Бена Войта об использовании оператора switch. Если несколько случаев в коммутаторе имеют один и тот же номер, вы получите ошибку компиляции.

Наиболее полезно для меня и, возможно, для оригинального постера, для этого не требуются какие-либо функции C ++.

Чтобы разобраться, я использовал ответ aaronps на Как мне избежать повторения при создании перечисления C ++ и зависимой структуры данных?

Во-первых, определите это где-нибудь в заголовке:

#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 создает сам тип данных enum. CHECK_ENUM создает тестовую функцию, которая включает все значения перечисления. Компилятор потерпит крах при компиляции CHECK_ENUM, если у вас есть дубликаты.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...