О перечислениях в C и C++
Современные стандарты C++ позволяет эффективно пользоваться перечислениями, предоставляя дополнительные синтаксические возможности.
Сегодня рассмотрим принципы работы с перечислений, от простого к сложному.
Перечисление представляет собой набор именованных целочисленных констант и объявляется следующим образом:
enum DaysOfTheWeek { saturday, sunday, monday, tuesday, wednesday, thursday, friday };
Здесь enum - ключевое слово, означающее, что здесь представлено объявление перечисления; DaysOfTheWeek - название объявляемого перечисления (в данном случае перечисление описывает дни недели); далее в фигурных скобках указываются названия элементов перечисления.
По умолчанию, нумерация элементов происходит в порядке возрастания, начиная с 0. Однако, можно указать индивидуальные значения для каждого элемента:
enum Keys { key1 = 5, key2 = 8, error = 0 };
Переменную, имеющую тип перечисления можно объявить и инициализировать следующим образом:
DaysOfTheWeek day = monday;
enum DaysOfTheWeek day = monday; //Для языка C
Как видно из примера, С++ имеет упрощённые синтаксические требования.
Кроме того, переменную или несколько переменных можно объявить непосредственно вместе с объявлением перечисления:
enum Keys { key1 = 5, key2 = 8, error = 0 } var1, var2;
Здесь var1 и var2 - названия переменных, имеющих тип перечисления Keys.
Всё перечисленное выше работает как в C, так и в C++.
Однако стандарт C++ предоставляет дополнительные возможности, которые рассмотрим далее.
Перечисления с ограниченной областью видимости
Описанные выше перечисления в современных реалиях можно называть «перечислениями в стиле C».
Начиная с версии C++11 есть возможность объявлять перечисления с ограниченной областью видимости. Вот пример такого объявления:
enum class DaysOfTheWeek { saturday, sunday, monday, tuesday, wednesday, thursday, friday};
Здесь в объявлении указывается не просто enum, а enum class. Разница проявляется при использовании такого перечисления:
DaysOfTheWeek day = DaysOfTheWeek::monday; //Верно
DaysOfTheWeek day = monday; //Теперь ошибка!
Кроме того, можно сделать так:
auto day = DaysOfTheWeek::monday; //автоматический вывод типа перечисления
То есть теперь элементы перечисления не находятся в той же области, где это перечисление объявлено (как происходило для перечислений в стиле C).
Указание базового типа перечисления
Начиная с C++11 появилась возможность указания базового типа для перечисления, то есть целочисленного типа, который используется для (внутреннего) представления значений перечисления. Например:
enum Keys : unsigned int { key1 = 5, key2 = 8, error = 0 };
Как видно из примера, базовый тип указывается после названия перечисления, через двоеточие. В данном случае указан unsigned int.
На практике польза указания базового типа весьма велика. Во-первых, это повышает кроссплатформенность приложений - так как на разных платформах значение базового типа по умолчанию может отличаться, и при определённых условиях, например при записи/чтении одного файла на различных устройствах (это особенно влияет, когда на разных платформах используются базовые типы разного байтового размера). Во-вторых, указав базовый тип, можно сократить потребление ресурсов памяти (если это позволяют значения самих элементов - от разрядности зависит максимальное значение, которое можно представить при помощи определённого типа), требуемых для хранения переменных, имеющих тип соответствующих перечислений, особенно если хранятся большие массивы таких значений.
Приведение к целым числам и обратно
Привести к целым числам и обратно можно как в стиле C (как при приведении обычных значений), так и при помощи static_cast (в C++). Пример:
int value = static_cast<int>(Keys::key2); // value == 8 (см. объявление enum Keys)
int value = (int)EnumValue; //приведение в стиле C (EnumValue - переменная перечислимого типа)
Кроме того, в C++ можно сделать так (не не в C!):
auto value = int(EnumValue);
Обратное преобразование, от целого числа к типу перечисления тоже не представляет сложности:
Keys value = static_cast<Keys>(8); // value == Keys::key2 (только C++)
Keys value = (Keys)8; // приведение в стиле C (но с синтаксисом C++)
enum Keys value = (enum Keys)8; // для языка C
Следует отметить, что в C++ перечисления без области видимости могут быть неявно преобразованы в int, но не наоборот.
Перечисления без перечислителей
Начиная с версии C++17 при определении перечисления с явным базовым типом и без перечислителей (т.е. без элементов перечисления), можно фактически ввести новый (пользовательский) целочисленный тип, который не имеет неявного преобразования в какой-либо другой тип (что таким образом в некоторых случаях даёт преимущества перед int и другими типами, которые могут быть неявно преобразованы там, где это противопоказано). Такой трюк позволяет избавиться от неявного преобразования там, где это может вызвать ошибки, например из-за человеческого фактора, когда разработчик мог бы допустить такое развитие событий по невнимательности.
Пример определения перечисления без перечислителей:
enum class byte : uint8_t { }; // подробнее об uint8_t читайте здесь
При этом новый тип является полной копией базового типа. В следствие этого его можно использовать в ABI без потери производительности (по сути, введение такого типа влияет только на слой API).
Инициализация переменной такого типа значением 42 имеет вид:
byte var{ 42 };
Либо:
auto var = byte{ 42 };
При попытке произвести сложение таких переменных неопытный пользователь получит ошибку:
byte a{5}, b{7};
auto c = a + b; //Ошибка!
Эта ошибка обусловлена тем, что по сути byte - новый тип, для которого не определены не только неявные преобразования, но и операторы преобразований, в частности сложения. К счастью, такой оператор можно определить самостоятельно:
byte operator+(byte a, byte b) {
return byte(uint8_t(a) + uint8_t(b)); // подробнее об uint8_t читайте здесь
}
Здесь определён оператор сложения для значений, имеющих тип byte. Отметим, что можно записать любую внутреннюю логику, так что оператор сложения может выполнять и умножение или какие-то другие более хитрые манипуляции (но на практике настолько грубые подмены крайне не рекомендуются, такая гибкость языка C++ была создана для более тонких целей).
byte a{5}, b{7};
auto c = a + b; // Теперь работает! ( c == byte{ 12 } )
