Безопасный способ преобразования целого числа в перечисление - программирование

Безопасный способ преобразования целого числа в перечисление

Что произойдет, если я передам целое число в класс enum, но значение отсутствует в перечислении? Например: Я хочу функцию, которая проверяет, имеет ли целое число какое-либо значение из класса enum:

enum class EnumClass { A, B = 4, C = 9, D = 60 };

bool checkEnumClass( int v )
{
    switch( static_cast< EnumClass >( v ) )
    {
    case EnumClass::A:
    case EnumClass::B:
    case EnumClass::C:
    case EnumClass::D:
        return true;
    default:
        return false;
    }
}

checkEnumClass( 0 ) == true;
checkEnumClass( 7 ) == false;   // is this true?

Правильно ли это проверить, является ли целое число конвертируемым для перечисления?

4b9b3361

Ответ 1

Перечисление может содержать любое значение между его наименьшими и наибольшими значениями, поэтому то, что у вас есть, в основном правильное. Единственное, что вам нужно сделать дополнительно, это убедиться, что целочисленный аргумент находится в правильном диапазоне, так как если вы попытаетесь применить int, находящийся вне диапазона перечисления, вы будете иметь поведение undefined:

bool checkEnumClass( int v )
{
    if (v < static_cast<int>(EnumClass::A)) return false;
    if (v > static_cast<int>(EnumClass::D)) return false;

    switch( static_cast< EnumClass >( v ) )
    {
    case EnumClass::A:
    case EnumClass::B:
    case EnumClass::C:
    case EnumClass::D:
        return true;
    default:
        return false;
    }
}

Ответ 2

Я не вижу принципиально лучшего решения, чем предложение, предлагаемое OP. Однако он имеет небольшой дефект, для которого я могу предложить (нестандартное) обходное решение.

Проблема заключается в следующем. Предположим, что сегодня код такой же, как в OP, но, однажды, кто-то добавляет новый перечислитель к EnumClass, который становится:

enum class EnumClass { A, B = 4, C = 9, D = 60, E = 70 };

Предположим также, что этот человек забывает обновить определение checkEnumClass (что вряд ли произойдет, особенно если код находится в другом файле). Тогда,

checkEnumClass( 70 );

вернет false, несмотря на то, что 70 теперь является допустимым значением. Модульные тесты могут помочь поймать эту ошибку, но человек должен помнить об обновлении теста. (Напомним, что они забыли обновить код в первую очередь!)

К сожалению, стандартный С++ не предлагает способ заставить switch на enum покрыть все случаи (в отличие от D, который предлагает final switch statement).

Однако существуют специфические для компилятора функции, которые могут сделать это для вас.

Для GCC (и, я считаю, Clang, также) вы можете добавить опцию компилятора -Wswitch (или -Wall, которая подразумевает -Wswitch). Для Visual Studio вы можете добавить

#pragma warning(error : 4062)

в файл, содержащий checkEnumClass (а не файл, содержащий определение перечисления)

Наконец, вы должны слегка изменить checkEnumClass, потому что метка default сообщает компилятору, что все случаи охвачены. Код должен выглядеть следующим образом:

bool checkEnumClass( int v )
{
    switch( static_cast< EnumClass >( v ) )
    {
    case EnumClass::A:
    case EnumClass::B:
    case EnumClass::C:
    case EnumClass::D:
        return true;
    }
    return false;
}

При таком обходном пути человек, включивший счетчик E, но забывший обновить checkEnumClass, соответственно получит следующую ошибку/предупреждение:

GCC:

предупреждение: значение перечисления 'E' не обрабатывается в переключателе [-Wswitch]

Visual Studio:

ошибка C4062: перечислитель 'E' в переключателе enum 'EnumClass' не обрабатывается
switch (static_cast <EnumClass> (v))

Обновление 1: после комментария elvis.dukaj.

Как хорошая практика добавьте опции -Werror в GCC, чтобы превратить все предупреждения в ошибки.

Обновление 2. Лучше, чем -Wswitch есть -Wswitch-enum, что поднимет предупреждение (или ошибку, если -Werror), даже если есть метка default. К сожалению, я не знаю подобной функции в Visual Studio.

Ответ 3

Если вам нужна проверка времени перемножения во время компиляции, вы можете попробовать следующее:

template <int I> struct check_enum { static const bool value = false; };

template <> struct check_enum<static_cast<int>(EnumClass::A)>
{ static const bool value = true; };

template <> struct check_enum<static_cast<int>(EnumClass::B)>
{ static const bool value = true; };

template <> struct check_enum<static_cast<int>(EnumClass::C)>
{ static const bool value = true; };

template <> struct check_enum<static_cast<int>(EnumClass::D)>
{ static const bool value = true; };

Затем вы можете использовать его следующим образом:

static_assert(check_enum<0>::value, "invalid enum value"); // ok!
static_assert(check_enum<1>::value, "invalid enum value"); // compile error

Живая демонстрация.

Изменить: тот же подход возможен с переменными шаблона С++ 14.

template <int I> constexpr bool check_enum = false;
template <> constexpr bool check_enum<static_cast<int>(EnumClass::A)> = true;
template <> constexpr bool check_enum<static_cast<int>(EnumClass::B)> = true;
template <> constexpr bool check_enum<static_cast<int>(EnumClass::C)> = true;
template <> constexpr bool check_enum<static_cast<int>(EnumClass::D)> = true;

static_assert(check_enum<0>, "invalid enum value"); // ok!
static_assert(check_enum<1>, "invalid enum value"); // compile error

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

Ответ 4

Просто убедитесь, что int не больше максимально возможного значения в вашем классе проверки, нет необходимости в команде switch, просто используйте оператор if или, еще лучше, просто bool.

bool checkEnumClass(int i)
{
    return (i <= 7);
}