Menu

Перечисления в C++: enum class против обычного enum

Изучите перечисления в C++: как их объявлять, почему областной enum class безопаснее обычного enum, пользовательские базовые значения, switch по перечислителям и преобразование в целые числа и обратно.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Для чего нужен enum

Структура группирует несколько связанных значений в один объект. enum решает другую задачу: он даёт имена небольшому фиксированному набору вариантов. Вместо того чтобы помнить, что 0 означает «красный», 1 — «зелёный», а 2 — «синий», вы пишете Color::Red, и компилятор не даёт вам ошибиться.

Прибегать к enum всякий раз, когда переменная может принимать лишь одно из горстки именованных состояний — цвет светофора, масть карты, статус соединения, — значит делать код самодокументируемым и позволять компилятору ловить опечатки и пропущенные случаи, которые голые целые числа никогда бы не отловили.

Объявление enum

В современном C++ есть две разновидности. Начните с той, к которой стоит прибегать почти всегда: областной enum class. Вы перечисляете имена, и к каждому перечислителю обращаются через имя перечисления с :::

Обратите внимание: вы пишете Color::Green, никогда — голый Green. Сами значения — это всего лишь метки: вы их сравниваете, присваиваете и передаёте, но базовое число вас редко интересует. По умолчанию Red равно 0, Green1, а Blue2, отсчёт идёт вверх от нуля.

Обычный enum против enum class

Более старый, необластной enum (без ключевого слова class) сбрасывает свои имена прямо в окружающую область видимости и сам преобразуется в int. Это звучит удобно, но порождает две настоящие проблемы:

enum Color { Red, Green, Blue };
enum Fruit { Apple, Banana, Red };  // error: 'Red' already declared

enum Status { Active, Inactive };
int x = Active;          // compiles silently - is this what you meant?
if (Active == Banana) {  // compares unrelated enums via int - allowed!
}

Поскольку обычные перечислители — это глобальные имена, два перечисления могут столкнуться лишь потому, что у них совпадает метка. А поскольку они вырождаются в int, компилятор спокойно сравнивает значения из совершенно не связанных перечислений. Областной enum class исправляет и то, и другое: имена живут внутри типа, а тип не превратится молча в int:

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

Пользовательские значения и базовый тип

Перечислителям можно присваивать явные числа. Те, что вы пропустите, продолжают счёт вверх от предыдущего, что удобно для таких вещей, как коды статуса HTTP или битовые флаги:

За каждым перечислением стоит целочисленный тип — по умолчанию int. Вы можете закрепить за ним меньший тип, когда важен размер, например при хранении множества перечислений в упакованной структуре или при соответствии формату передачи данных:

Выбор базового типа также гарантирует диапазон, в который должны помещаться ваши значения: uint8_t-перечисление не может хранить значение выше 255.

Преобразование между enum и int

Областной enum class никогда не преобразуется неявно — в этом весь смысл. Когда число действительно нужно — чтобы вывести его, проиндексировать массив или прочитать из файла, — прибегайте к static_cast. Переход от enum к int всегда безопасен:

Преобразовать int обратно в перечисление — вот опасное направление. Приведение не проверяет, что число соответствует реальному перечислителю: оно выдаст вам значение, технически находящееся вне именованного набора перечисления:

Suit s = static_cast<Suit>(2);   // fine - that's Clubs
Suit bad = static_cast<Suit>(99); // compiles, but 99 is not a valid Suit
// using `bad` in a switch or as an array index is a lurking bug

Если целое число приходит из пользовательского ввода или файла, проверьте диапазон самостоятельно перед приведением, иначе вы создадите значение, которое ни один case не обрабатывает, — скрытый источник неопределённого поведения в дальнейшем.

Использование enum со switch

Поскольку перечисление — это «одно из фиксированного набора», оно идеально сочетается со switch. Когда вы покрываете каждый перечислитель, многие компиляторы предупредят вас, если позже вы добавите новое значение и забудете его обработать, — бесплатная безопасность, которой не дают сырые целые числа:

Одна ловушка: встроенного способа вывести имя перечислителя нет. cout << TrafficLight::Red не скомпилируется для областного перечисления, а даже для обычного enum выведет число, а не «Red». Небольшой switch или таблица соответствия, как выше, — обычный способ превратить перечисление в человекочитаемую строку.

Далее: Исключения

Перечисления и структуры позволяют моделировать, как выглядят ваши данные. Но реальным программам также приходится иметь дело с тем, что что-то идёт не так: файл, который не открывается, число, которое не разбирается, значение вне диапазона. C++ обрабатывает эти пути ошибок с помощью исключений, и это тема следующей страницы.

Часто задаваемые вопросы

В чём разница между enum и enum class в C++?

Обычный enum «протекает» своими именами в окружающую область видимости и неявно преобразуется в int, что приводит к конфликтам имён и случайным сравнениям. Областной enum class держит свои имена внутри перечисления (Color::Red) и отказывается преобразовываться в int без явного приведения. В современном C++ предпочитайте enum class: он типобезопасен и избегает классических ловушек.

Как преобразовать enum в C++ в int?

Обычный enum преобразуется неявно, поэтому int n = Red; просто работает. Областной enum class требует явного приведения: int n = static_cast<int>(Color::Red);. Чтобы пойти в обратную сторону, приведите int назад: Color c = static_cast<Color>(2);, но будьте осторожны: во время выполнения не проверяется, что значение является допустимым перечислителем.

С какого значения начинается первый enum в C++?

По умолчанию первый перечислитель равен 0, а каждый следующий на единицу больше предыдущего. Так что в enum class Level { Low, Mid, High }; Low равно 0, Mid равно 1, а High равно 2. Вы можете присвоить любому из них явные значения, чтобы переопределить это поведение.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ