Введение
Перечисления дают именованные, осмысленные альтернативы «магическим числам» — в этой статье разберём 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.