[Перевод] Эволюция enum
Константы — это здорово. Типы — это замечательно. Константы определенного типа — еще лучше. А enum
классы — это просто фантастика.
В прошлом году мы говорили о том, почему следует избегать использования булевых параметров функций. Одно из предложенных решений — использование сильных типов, в частности, использование перечислений вместо голых булевых значений. В этот раз давайте посмотрим, как развивались перечисления и связанная с ними поддержка на протяжении всего жизненного пути нашего любимого языка.
Unscoped перечисления
Перечисления являются частью оригинального языка C++, фактически, они были заимствованы из C. Перечисления — это отдельные типы с ограниченным диапазоном значений. Диапазон значений ограничивается некоторыми явно прописанными константами. Ниже приведен пример классического перечисления:
enum Color { red, green, blue };
Посмотрев на этот небольшой пример, можно обратить внимание на две вещи:
Само перечисление (
enum
) — это существительное в единственном числе, даже несмотря на то, что обычно оно перечисляет несколько значений. Мы используем такое соглашение, потому что понимаем, что оно всегда будет использоваться с одним значением. Если вы берете параметр функцииColor
, то будет взят один цвет. Когда вы сравниваете со значением, вы будете сравнивать с одним значением. Например, логичнее сравнивать сColor::red
, чем сColors::red
.Значения перечислителей прописываются в нижнем регистре, а не капсом (ALL_CAPS)! Хотя есть большая вероятность, что вы привыкли писать их именно капсом. Я тоже так делал. Почему же я в итоге изменил своей традиции? Потому что для написания этой статьи я проверил core guidelines, и в Enum.5 четко сказано, что мы не должны использовать капс, чтобы избежать конфликтов с макросами. Кстати, в Enum.1 также сказано, что мы должны использовать перечисления, а не макросы.
Начиная с C++11, количество возможностей для объявления enum увеличилось. В C++11 появилась возможность указывать базовый тип (underlying type) перечисления. Если он не определен, то базовый тип определяется реализацией, но в любом случае это будет целочисленный тип.
Как его определить? С точки зрения синтаксиса это будет выглядеть немного похожим на наследование! Хотя здесь не нужно определять уровни доступа:
enum Color : int { red, green, blue };
Благодаря этому вы можете быть уверены в том, какой тип будет базовым. Тем не менее, это должен быть целочисленный тип! Например, это не может быть строка. Если вы попробуете указать ее в качестве базового типа, то получите явное сообщение об ошибке:
main.cpp:4:19: error: underlying type 'std::string' {aka 'std::__cxx11::basic_string'} of 'Color' must be an integral type
4 | enum Color : std::string { red, green, blue };
| ^~~~~~
Обратите внимание, что core guidelines не рекомендуют использовать эту практику! Указывать базовый тип следует только в том случае, если это необходимо.
Когда это может быть необходимо? Можно выделить две распространенные ситуации:
Если вы знаете, что количество вариантов будет очень ограничено, и хотите сэкономить немного памяти:
enum Direction : char { north, south, east, west,
northeast, northwest, southeast, southwest };
Или, если вы делаете предварительное объявление (forward declare)
enum
, вы также должны объявить и тип:
enum Direction : char;
void navigate(Direction d);
enum Direction : char { north, south, east, west,
northeast, northwest, southeast, southwest };
Вы также можете указать точное значение одного или всех перечисляемых значений, если они являются constexpr
.
enum Color : int { red = 0, green = 1, blue = 2 };
И снова core guidelines рекомендует не делать этого, если на это нет особой необходимости! Как только вы начнете это делать, можно легко ошибиться и все испортить. В любом случае, мы можем рассчитывать на то, что компилятор присвоит последующие значения последующим значениям перечислителя.
Хорошим юзкейсом для указания значения перечислителя является определение только начального значения. Например, если вы определяете месяцы и не хотите начинать с нуля.
enum Month { jan = 1, feb, mar, apr, may, jun,
jul, august, sep, oct, nov, dec };
Другая причина может заключаться в том, что вы хотите определить значения в виде некоторого значимого символа
enum altitude: char {
high = 'h',
low = 'l'
};
Еще одной причиной может быть эмуляция некоторых битовых полей. То есть вам не нужны последующие значения, вместо этого вы всегда хотите получать следующую степень двойки
enum access_type { read = 1, write = 2, exec = 4 };
Scoped перечисления
В предыдущем разделе мы видели объявления типа enum
EnumName{};
. В C++11 появился новый тип перечислений, называемый scoped
(с ограничением на область видимости) enum
. Они объявляются либо с помощью ключевых слов class
, либо struct
(между ними нет никакой разницы).
Их синтаксис следующий:
enum class Color { red, green, blue };
Для scoped
перечислений базовый тип по умолчанию определен в стандарте, и это int
. Это также означает, что если вы хотите предварительно объявить перечисление, вам не нужно указывать базовый тип. Если он должен быть int
, то этого будет достаточно:
enum class Color;
Но какие еще существуют различия, помимо синтаксических различий между способами их объявления?
Unscoped перечисления могут быть неявно преобразованы к своему базовому типу. Неявные преобразования — это часто не то, что вам нужно, и у scoped
перечислений нет такой «особенности». Именно из-за нежелательности неявных преобразований core guidelines настоятельно рекомендует использовать scoped, а не unscoped перечисления.
void Print_color(int color);
enum Web_color { red = 0xFF0000, green = 0x00FF00, blue = 0x0000FF };
enum Product_info { red = 0, purple = 1, blue = 2 };
Web_color webby = Web_color::blue;
// Clearly at least one of these calls is buggy.
Print_color(webby);
Print_color(Product_info::blue);
Unscoped
перечисления экспортируют свои перечислители в объемлющую область видимости, что может привести к конфликту имен. С другой стороны, при использовании scoped
перечислений вы всегда должны указывать имя перечисления вместе с перечислителями.
enum UnscopedColor { red, green, blue };
enum class ScopedColor { red, green, blue };
int main() {
[[maybe_unused]] UnscopedColor uc = red;
// [[maybe_unused]] ScopedColor sc = red; // Doesn't compile
[[maybe_unused]] ScopedColor sc = ScopedColor::red;
}
Что еще?
Теперь, когда мы разобрались, как работают scoped/unscoped
перечисления и чем они отличаются друг от друга, давайте посмотрим, какие еще функции, связанные с перечислениями, предлагает язык или стандартная библиотека.
std: is_enum
В C++11 появился заголовок
. Он включает утилиты для проверки свойств типов. Неудивительно, что is_enum
проверяет, является ли тип перечислением или нет. Он возвращает true
как для scoped
, так и для unscoped
версий.
Начиная с C++17, также доступен более удобный is_enum_v
.
#include
#include
enum UnscopedColor { red, green, blue };
enum class ScopedColor { red, green, blue };
struct S{};
int main() {
std::cout << std::boolalpha
<< std::is_enum::value << '\n'
<< std::is_enum::value << '\n'
<< std::is_enum_v << '\n';
}
std: underlying_type
Также в C++11 был добавлен std::underlying_type
. Он помогает нам получить базовый тип перечисления. До C++20, если проверяемое перечисление не полностью определено или не является перечислением, поведение не определено. Начиная с C++, программа становится некорректной для неполных типов перечислений.
В C++14 появился хелпер std::underlying_type_t
.
#include
#include
enum UnscopedColor { red, green, blue };
enum class ScopedColor { red, green, blue };
enum class CharBasedColor : char { red = 'r', green = 'g', blue = 'b' };
int main() {
constexpr bool isUnscopedColorInt = std::is_same_v< std::underlying_type::type, int >;
constexpr bool isScopedColorInt = std::is_same_v< std::underlying_type_t, int >;
constexpr bool isCharBasedColorInt = std::is_same_v< std::underlying_type_t, int >;
constexpr bool isCharBasedColorChar = std::is_same_v< std::underlying_type_t, char >;
std::cout
<< "underlying type for 'UnscopedColor' is " << (isUnscopedColorInt ? "int" : "non-int") << '\n'
<< "underlying type for 'ScopedColor' is " << (isScopedColorInt ? "int" : "non-int") << '\n'
<< "underlying type for 'CharBasedColor' is " << (isCharBasedColorInt ? "int" : "non-int") << '\n'
<< "underlying type for 'CharBasedColor' is " << (isCharBasedColorChar ? "char" : "non-char") << '\n'
;
}
Объявление перечислений с использованием using в C++20
Начиная с C++20, с перечислениями можно использовать using
. Он вводит имена перечислителей в заданную область видимости.
Эта фича достаточно умна, чтобы выдавать ошибку компиляции в случае, если при следующем using
будет добавлено имя перечислителя, которое уже было введено в другом перечислении.
#include
enum class ScopedColor { red, green, blue };
enum class CharBasedColor : char { red = 'r', green = 'g', blue = 'b' };
int main() {
using enum ScopedColor; // OK!
using enum CharBasedColor; // error: 'CharBasedColor CharBasedColor::red' conflicts with a previous declaration
}
Стоит отметить, что компилятор не распознает, если в unscoped
перечислении уже есть имя перечислителя, которое уже было введено в данное пространство имен. В следующем примере из UnscopedColor
уже доступны red
, green
и blue
, однако using
ScopedColor
с теми же именами перечислителей спокойно принимается принимается.
#include
enum UnscopedColor { red, green, blue };
enum class ScopedColor { red, green, blue };
int main() {
using enum ScopedColor;
}
C++23 принесет std: is_scoped_enum
В C++23 в заголовке
появится еще одна функция, связанная с перечислениями, — std::is_scoped_enum
и ее вспомогательная функция std::is_scoped_enum_v
. Как следует из названия и подтверждается приведенным ниже фрагментом, она проверяет, является ли ее аргумент scoped
перечислением или нет.
#include
#include
enum UnscopedColor { red, green, blue };
enum class ScopedColor { red, green, blue };
struct S{};
int main()
{
std::cout << std::boolalpha;
std::cout << std::is_scoped_enum::value << '\n';
std::cout << std::is_scoped_enum_v << '\n';
std::cout << std::is_scoped_enum_v << '\n';
std::cout << std::is_scoped_enum_v << '\n';
}
/*
false
true
false
false
*/
Если вы хотите попробовать фичи C++23, используйте флаг компилятора -std=c++2b
.
В C++23 также появится std: to_underlying
В C++23 появится еще одна библиотечная функция для перечислений. Заголовок
разживется функцией std::to_underlying
. Она преобразует перечисление в его базовый тип. Как уже говорилось, это библиотечная функция, то есть она может быть реализована в более ранних версиях.
Ее можно заменить на static_cast
, если у вас есть доступ только к более ранним версиям: static_cast
.
#include
#include
#include
enum class ScopedColor { red, green, blue };
int main()
{
ScopedColor sc = ScopedColor::red;
[[maybe_unused]] int underlying = std::to_underlying(sc);
[[maybe_unused]] int underlyingEmulated = static_cast>(sc);
[[maybe_unused]] std::underlying_type_t underlyingDeduced = std::to_underlying(sc);
}
Напоминаю, что если вы хотите опробовать фичи C++23, используйте флаг компилятора -std=c++2b
.
Заключение
В этой статье мы обсудили все фичи языка и стандартной библиотеки, связанные с перечислениями. Мы увидели, чем отличаются scoped
и unscoped
перечисления и почему все-таки лучше использовать scoped вариант. И это не единственная рекомендация из Core Guidelines, которую мы обсудили.
Мы также посмотрели, как за эти годы обогатилась стандартная библиотека, упростив работу с перечислениями. А еще мы немного заглянули в будущее и узнали, что принесет нам C++23.
В заключение приглашаем всех начинающих разработчиков на C++ на открытое занятие, посвященное полиморфизму в C++. Чему научимся:
— в каких случаях полезно использовать полиморфизм;
— как работать с виртуальными функциями в C++;
— какая цена использования виртуальных функций.Урок пройдет вечером 19 декабря, регистрируйтесь по ссылке.