Подтвердить что ты не робот

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 и С++, лучше.

4b9b3361

Ответ 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, если у вас есть дубликаты.