Для чего нужен enum
Структура группирует несколько связанных значений в один объект. enum решает другую задачу: он даёт имена небольшому фиксированному набору вариантов. Вместо того чтобы помнить, что 0 означает «красный», 1 — «зелёный», а 2 — «синий», вы пишете Color::Red, и компилятор не даёт вам ошибиться.
Прибегать к enum всякий раз, когда переменная может принимать лишь одно из горстки именованных состояний — цвет светофора, масть карты, статус соединения, — значит делать код самодокументируемым и позволять компилятору ловить опечатки и пропущенные случаи, которые голые целые числа никогда бы не отловили.
Объявление enum
В современном C++ есть две разновидности. Начните с той, к которой стоит прибегать почти всегда: областной enum class. Вы перечисляете имена, и к каждому перечислителю обращаются через имя перечисления с :::
Обратите внимание: вы пишете Color::Green, никогда — голый Green. Сами значения — это всего лишь метки: вы их сравниваете, присваиваете и передаёте, но базовое число вас редко интересует. По умолчанию Red равно 0, Green — 1, а Blue — 2, отсчёт идёт вверх от нуля.
Обычный 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. Вы можете присвоить любому из них явные значения, чтобы переопределить это поведение.