Синтаксис си/с++

Перечисления: enum в C и enum class в C++

2 просмотров

Введение

Перечисления дают именованные, осмысленные альтернативы «магическим числам» — в этой статье разберём enum в его классическом, С-совместимом виде, и более современный, безопасный enum class C++11, устраняющий ряд практических проблем классических перечислений.

Концепция

Классический enum (наследуемый из C) объявляет набор именованных целочисленных констант — каждое значение неявно преобразуется в int и обратно, что удобно для совместимости с C-кодом, но создаёт риск случайного смешивания значений из разных, несвязанных перечислений или прямого сравнения с произвольными целыми числами без какой-либо проверки типов компилятором. enum class (C++11) устраняет это неявное преобразование — значения enum class не неявно преобразуются в int и не сравниваются напрямую со значениями другого enum class без явного приведения типа, что значительно повышает типобезопасность за счёт небольшого снижения удобства (требующего явного приведения там, где оно действительно нужно).

Пример кода

#include <iostream>

// Классический enum — неявное преобразование в int, общая область видимости имён
enum Color { RED, GREEN, BLUE }; // RED=0, GREEN=1, BLUE=2 по умолчанию

enum Status { ACTIVE, INACTIVE }; // ПРОБЛЕМА: ACTIVE тоже равно 0 — конфликт ИМЁН в общей области видимости!

int main()
{
    Color favoriteColor = RED;
    int colorAsInt = favoriteColor; // неявное преобразование в int — допустимо БЕЗ явного приведения

    if (favoriteColor == 0) { // сравнение enum с "магическим числом" — компилятор НЕ предупреждает об этом
        std::cout << "Это красный (или просто число 0?)" << std::endl;
    }

    return 0;
}
// enum class — типобезопасная альтернатива, устраняющая проблемы классического enum
#include <iostream>

enum class TrafficLightColor { Red, Yellow, Green };
enum class Status { Active, Inactive }; // Status::Active НЕ конфликтует с TrafficLightColor::Red

int main()
{
    TrafficLightColor light = TrafficLightColor::Red; // ОБЯЗАТЕЛЬНО полное имя — TrafficLightColor::Red, не просто Red

    // int colorAsInt = light; // ОШИБКА КОМПИЛЯЦИИ — enum class НЕ преобразуется неявно в int!
    int colorAsInt = static_cast<int>(light); // нужно ЯВНОЕ приведение через static_cast

    if (light == TrafficLightColor::Red) { // сравнение возможно ТОЛЬКО с тем же enum class — типобезопасно
        std::cout << "Красный свет" << std::endl;
    }

    return 0;
}
// enum class с явно заданным базовым типом — контроль размера представления
enum class ErrorCode : unsigned char { None = 0, NotFound = 1, AccessDenied = 2 };
// явное указание ": unsigned char" — занимает 1 байт вместо типичного int (4 байта) по умолчанию

Пояснения к коду

Конфликт имён RED/ACTIVE (оба со значением 0) в классическом enum показывает практическую проблему — имена значений классических перечислений попадают в общую, окружающую область видимости (а не «вложены» внутрь самого перечисления), что создаёт риск конфликта имён между разными, не связанными друг с другом перечислениями, объявленными в одной и той же области видимости программы. enum class TrafficLightColor/Status показывает решение этой проблемы — обращение требует полного, явного имени (TrafficLightColor::Red), что одновременно устраняет конфликт имён и делает каждое использование значения перечисления более явным и понятным при чтении кода без необходимости заглядывать в объявление, к какому конкретно перечислению относится используемое имя. Явное указание базового типа (: unsigned char) для ErrorCode показывает дополнительную возможность enum class — контроль над размером представления перечисления, что особенно ценно для embedded-контекста (статьи 351-360) или случаев, где размер структуры, содержащей такое перечисление, критичен (статья 521 про выравнивание).

Подводные камни

  • Использование классического enum в новом коде «по привычке», без рассмотрения enum class — для нового кода, не требующего совместимости с C-API или унаследованным C++ кодом, enum class почти всегда предпочтительнее благодаря устранению конфликтов имён и предотвращению случайных, неконтролируемых неявных преобразований в int, и сознательный выбор классического enum должен быть мотивирован конкретной необходимостью (взаимодействие с C-кодом, потребность в неявном преобразовании в int для конкретного, обоснованного случая использования).
  • Сравнение значения классического enum с «магическим числом» (favoriteColor == 0 вместо favoriteColor == RED) — хотя это формально допустимо благодаря неявному преобразованию, такой код менее читаем и более подвержен ошибкам при последующем изменении порядка или значений констант перечисления по сравнению с явным сравнением с именованной константой, и эта же категория проблем полностью устранена для enum class, не допускающего такого сравнения вовсе без явного приведения.
  • Предположение о конкретных, гарантированных числовых значениях элементов enum/enum class без явного их указания, особенно при сериализации значения перечисления для хранения или передачи (статья основного цикла про сериализацию) — без явного указания значений (enum class Status { Active = 1, Inactive = 2 }) компилятор присваивает значения автоматически, начиная с 0 и последовательно увеличивая на единицу, и при последующем изменении порядка объявления элементов перечисления (добавление нового элемента в середину списка) эти автоматически присвоенные значения изменятся для всех последующих элементов, что может нарушить совместимость с уже сохранёнными или переданными ранее данными, рассчитывавшими на предыдущие, более не актуальные числовые значения.
  • Избыточно частое явное приведение enum class к int через static_cast, фактически обходя задуманную типобезопасность этой конструкции — если код приложения постоянно вынужден явно приводить значения enum class к int для своей обычной логики (а не для редких, действительно необходимых случаев, как вывод в лог или сериализация), это может говорить о том, что выбор enum class для конкретного случая был не вполне обоснован, и возможно, что-то в архитектуре кода стоит пересмотреть, либо, в действительно обоснованных случаях обоснованной частой потребности в целочисленном представлении, рассмотреть классический enum.