Константы — это здорово. Типы — это замечательно. Константы определенного типа — еще лучше. А 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<char>'} 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 появился заголовок <type_traits>. Он включает утилиты для проверки свойств типов. Неудивительно, что is_enum проверяет, является ли тип перечислением или нет. Он возвращает true как для scoped, так и для unscoped версий.
Начиная с C++17, также доступен более удобный is_enum_v.
#include <iostream> #include <type_traits> enum UnscopedColor { red, green, blue }; enum class ScopedColor { red, green, blue }; struct S{}; int main() { std::cout << std::boolalpha << std::is_enum<UnscopedColor>::value << '\n' << std::is_enum<ScopedColor>::value << '\n' << std::is_enum_v<S> << '\n'; }
std::underlying_type
Также в C++11 был добавлен std::underlying_type. Он помогает нам получить базовый тип перечисления. До C++20, если проверяемое перечисление не полностью определено или не является перечислением, поведение не определено. Начиная с C++, программа становится некорректной для неполных типов перечислений.
В C++14 появился хелпер std::underlying_type_t.
#include <iostream> #include <type_traits> 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<UnscopedColor>::type, int >; constexpr bool isScopedColorInt = std::is_same_v< std::underlying_type_t<ScopedColor>, int >; constexpr bool isCharBasedColorInt = std::is_same_v< std::underlying_type_t<CharBasedColor>, int >; constexpr bool isCharBasedColorChar = std::is_same_v< std::underlying_type_t<CharBasedColor>, 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 <type_traits> 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 <type_traits> enum UnscopedColor { red, green, blue }; enum class ScopedColor { red, green, blue }; int main() { using enum ScopedColor; }
C++23 принесет std::is_scoped_enum
В C++23 в заголовке <type_traits> появится еще одна функция, связанная с перечислениями, — std::is_scoped_enum и ее вспомогательная функция std::is_scoped_enum_v. Как следует из названия и подтверждается приведенным ниже фрагментом, она проверяет, является ли ее аргумент scoped перечислением или нет.
#include <iostream> #include <type_traits> 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<UnscopedColor>::value << '\n'; std::cout << std::is_scoped_enum_v<ScopedColor> << '\n'; std::cout << std::is_scoped_enum_v<S> << '\n'; std::cout << std::is_scoped_enum_v<int> << '\n'; } /* false true false false */
Если вы хотите попробовать фичи C++23, используйте флаг компилятора -std=c++2b.
В C++23 также появится std::to_underlying
В C++23 появится еще одна библиотечная функция для перечислений. Заголовок <utility> разживется функцией std::to_underlying. Она преобразует перечисление в его базовый тип. Как уже говорилось, это библиотечная функция, то есть она может быть реализована в более ранних версиях.
Ее можно заменить на static_cast, если у вас есть доступ только к более ранним версиям: static_cast<std::underlying_type_t<MyEnum>>(e);.
#include <iostream> #include <type_traits> #include <utility> 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<std::underlying_type_t<ScopedColor>>(sc); [[maybe_unused]] std::underlying_type_t<ScopedColor> underlyingDeduced = std::to_underlying(sc); }
Напоминаю, что если вы хотите опробовать фичи C++23, используйте флаг компилятора —std=c++2b.
Заключение
В этой статье мы обсудили все фичи языка и стандартной библиотеки, связанные с перечислениями. Мы увидели, чем отличаются scoped и unscoped перечисления и почему все-таки лучше использовать scoped вариант. И это не единственная рекомендация из Core Guidelines, которую мы обсудили.
Мы также посмотрели, как за эти годы обогатилась стандартная библиотека, упростив работу с перечислениями. А еще мы немного заглянули в будущее и узнали, что принесет нам C++23.
В заключение приглашаем всех начинающих разработчиков на C++ на открытое занятие, посвященное полиморфизму в C++. Чему научимся:
— в каких случаях полезно использовать полиморфизм;
— как работать с виртуальными функциями в C++;
— какая цена использования виртуальных функций.Урок пройдет вечером 19 декабря, регистрируйтесь по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/781712/
Добавить комментарий