Перечисления C/C++: Обнаружьте, когда несколько объектов отобразятся на то же значение

Существует ли время компиляции, способ обнаружить / предотвращает дублирующиеся значения в рамках перечисления 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++, лучше.

22
задан Dan 5 April 2010 в 04:04
поделиться

5 ответов

Есть несколько способов проверить это время компиляции, но они не всегда могут сработать для вас. Начните с вставки значения перечисления «маркер» прямо перед 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.");
 ^
14
ответ дан 29 November 2019 в 05:21
поделиться

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

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;
3
ответ дан 29 November 2019 в 05:21
поделиться

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

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

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

3
ответ дан 29 November 2019 в 05:21
поделиться

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

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

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

1
ответ дан 29 November 2019 в 05:21
поделиться

Я не увидел в ваших требованиях слова "красивый", поэтому предлагаю следующее решение, реализованное с помощью библиотеки 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.

7
ответ дан 29 November 2019 в 05:21
поделиться