Scoped enums в C++: почему enum class безопаснее, чем старый enum?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему enum class — ваш новый лучший друг в C++?

Каждый, кто начинал изучать C++ или возвращался к нему после перерыва, рано или поздно сталкивается с дилеммой: использовать старый добрый enum или его более современного собрата enum class? На Stack Overflow вопрос «Why is enum class considered safer to use than plain enum?» собрал тысячи просмотров — и это не случайно. Переход на scoped enum (именно так официально называется enum class) — это не просто дань моде, а реальный шаг к более надёжному и читаемому коду.

Давайте разберёмся, в чём именно кроется опасность старого enum и как enum class помогает избежать целого класса ошибок. Если вы пишете на C++ и всё ещё используете enum без области видимости — эта статья для вас.

Топ-3 ошибки старого enum (и как их исправляет enum class)

1️⃣ Загрязнение глобальной области видимости

Самая распространённая проблема старого enum — его константы «вываливаются» в окружающее пространство имён. Представьте, что у вас есть два перечисления:

// Плохо: старый enum
enum Color { Red, Green, Blue };
enum Feeling { Happy, Sad, Blue }; // Ошибка компиляции! Blue уже определён

Константа Blue из Color уже занята, и вы не можете использовать то же имя в Feeling. В большом проекте с сотнями файлов это приводит к настоящей головной боли. Приходится придумывать длинные имена вроде Color_Blue или COLOR_BLUE, что уродует код.

Решение с enum class:

// Хорошо: enum class (scoped enum)
enum class Color { Red, Green, Blue };
enum class Feeling { Happy, Sad, Blue }; // ОК! Конфликта нет

Теперь Blue живёт внутри своего класса. Чтобы обратиться к нему, нужно писать Color::Blue или Feeling::Blue. Это не только решает проблему коллизий, но и делает код самодокументируемым — сразу видно, к какому типу относится значение.

2️⃣ Неявное преобразование к int (и обратно)

Старый enum неявно приводится к целочисленным типам. На первый взгляд это удобно, но на практике — источник трудноуловимых багов.

// Опасный старый enum
enum WarningLevel { Low, Medium, High };

void setVolume(int level) {
    if (level > 10) level = 10;
    // ...
}

setVolume(High); // Работает! Но High = 2, а не уровень громкости
// Вы случайно передали уровень тревоги вместо громкости — компилятор молчит

Компилятор проглотит такое без единого предупреждения. А если вы перепутаете порядок аргументов в функции — баг уйдёт в рантайм.

Как защищает enum class:

enum class WarningLevel { Low, Medium, High };

void setVolume(int level);

setVolume(WarningLevel::High); // Ошибка компиляции!
// Нет неявного преобразования из enum class в int

Компилятор заставит вас явно преобразовать значение, если это действительно нужно. А это значит, что вы осознаёте каждое преобразование, а не полагаетесь на «авось пронесёт».

// Если очень нужно — делаем явно
setVolume(static_cast<int>(WarningLevel::High)); // Явно и понятно

3️⃣ Неопределённый базовый тип

Старый enum не гарантирует, какой именно целочисленный тип будет использоваться под капотом. Компилятор выбирает его сам (обычно int), и это может привести к неожиданностям при передаче между разными компиляторами или платформами.

// Размер может быть разным!
enum Small { A, B, C }; // Может быть int, может быть char
enum Big { X = 100000, Y, Z }; // Всё ещё может быть int

С enum class вы можете явно указать базовый тип, что даёт полный контроль над размером и поведением:

enum class Small : uint8_t { A, B, C }; // Точно 1 байт
enum class Big : int64_t { X = 100000, Y, Z }; // Точно 8 байт

Это особенно важно при работе с сериализацией данных, сетевыми протоколами или встраиваемыми системами, где размер имеет значение.

А что с обратной совместимостью? Нужно ли переписывать всё?

Если вы работаете с легаси-кодом, где повсюду старые enum, не спешите хвататься за голову. enum class не ломает старый код — вы можете вводить его постепенно, модуль за модулем. Более того, старый enum и enum class могут прекрасно сосуществовать в одном проекте.

Единственный случай, когда старый enum оправдан — это если вы используете его как битовые флаги и активно работаете с побитовыми операциями. Но и тут есть нюанс: начиная с C++23, enum class можно использовать с битовыми масками, если переопределить операторы. А до C++23 можно обойтись явными преобразованиями.

Практический пример: рефакторинг кода

Допустим, у вас есть такой фрагмент:

enum Status { OK, Error, Pending };

void process(Status s) {
    if (s == 0) { // Ошибка! Сравнение с int
        // ...
    }
}

Здесь программист поленился и сравнил Status с числом. Если кто-то поменяет порядок констант — код сломается. Исправляем:

enum class Status { OK, Error, Pending };

void process(Status s) {
    if (s == Status::OK) { // Чётко и безопасно
        // ...
    }
}

Теперь никаких магических чисел, никаких случайных сравнений с int. Код стал понятнее и надёжнее.

Когда enum class может быть неудобен?

Давайте честно: у enum class есть пара моментов, которые могут раздражать новичков:

  • Нужно писать имя класса перед каждым значением — это делает код многословнее. Но это же и плюс: читаемость повышается.
  • Нельзя использовать в switch с implicit conversion — но это и хорошо, вы не перепутаете типы.
  • Нужно явно приводить к int — для битовых флагов приходится писать static_cast.

Однако все эти «неудобства» — на самом деле защита от ошибок. Привыкнув к enum class, вы уже не захотите возвращаться к старому стилю.

Краткий чек-лист: что выбрать?

Блоги

Рекомендаций по книгам