У перечислений есть множество способов применения в разработке. Например, при создании игр они используются для программирования состояний персонажа или возможных направлений движения:
enum State {Idle, Fidget, Walk, Scan, Attack}; enum Direction {North, South, East, West};
Гораздо удобнее, когда во время отладки в консоль выводится сообщение типа “State: Fidget
” вместо “State: 1
”. Также частенько бывает нужно сериализировать перечисления в JSON, YAML или иной формат, причём в виде строковых значений. Помимо того, что строковые воспринимать легче, чем числа, их применение в формате сериализации повышает устойчивость к изменениям численных значений констант перечислений. В идеале, "Fidget"
должен ссылаться на Fidget
, даже если объявлена новая константа, а Fidget
имеет значение, отличное от 1.
К сожалению, в С++ нет возможности легко конвертировать значения перечислений в строковые и обратно. Поэтому разработчики вынуждены прибегать к разным ухищрениям, которые требуют определённой поддержки: жёстко закодированным преобразованиям или к использованию неприглядного ограничительного синтаксиса, наподобие Х-макросов. Кто-то дополнительно использует средства сборки для автоматического преобразования. Естественно, это только усложняет процесс разработки. Ведь перечисления имеют свой собственный синтаксис и хранятся в собственных входных файлах, что не облегчает работу средств сборки в Makefile или файлах проекта.
Однако средствами С++ можно гораздо проще решить задачу преобразования перечислений в строковые.
Есть возможность избежать всех упомянутых трудностей и генерировать перечисления с полной рефлексией на чистом С++. Объявление выглядит так:
BETTER_ENUM(State, int, Idle, Fidget, Walk, Scan, Attack) BETTER_ENUM(Direction, int, North, South, East, West)
Способ применения:
State state = State::Fidget; state._to_string(); // "Fidget" std::cout << "state: " << state; // Пишет "state: Fidget" state = State::_from_string("Scan"); // State::Scan (3) // Применяется в switch, как и обычное перечисление. switch (state) { case State::Idle: // ... break; // ... }
Это делается с помощью нескольких ухищрений, связанных с препроцессором и шаблоном. О них мы немного поговорим в конце статьи.
Помимо преобразования в строковые и обратно, а также поточного ввода/вывода, мы можем ещё и перебирать сгенерированные перечисления:
for (Direction direction : Direction._values()) character.try_moving_in_direction(direction);
Можно сгенерировать перечисления с разреженными диапазонами, а затем подсчитать:
BETTER_ENUM(Flags, char, Allocated = 1, InUse = 2, Visited = 4, Unreachable = 8) Flags::_size(); // 4
Если вы работаете в С++ 11, то можете даже сгенерировать код на основе перечислений, потому что все преобразования и циклы могут выполняться в ходе компиляции с помощью функций constexpr
. Можно, к примеру, написать такую функцию constexpr
, которая будет вычислять максимальное значение перечисления и делать его доступным во время компиляции. Даже если значения констант выбираются произвольно и не объявляются в порядке возрастания.
Вы можете скачать с Github пример реализации макроса, упакованного в библиотеку под названием Better Enums (Улучшенные перечисления). Она распространяется под лицензией BSD, так что с ней можно делать что угодно. В данной реализации имеется один заголовочный файл, так что использовать её очень просто, достаточно добавить enum.h
в папку проекта. Попробуйте, возможно, это поможет вам в решении ваших задач.
Как это работает
Для осуществления преобразований между строковыми и значениями перечислений необходимо сгенерировать соответствующий маппинг. Better Enums делает это с помощью создания двух массивов в ходе компиляции. Например, если у вас есть такое объявление:
BETTER_ENUM(Direction, int, North = 1, South = 2, East = 4, West = 8)
то макрос переделает его в нечто подобное:
struct Direction { enum _Enum : int {North = 1, South = 2, East = 4, West = 8}; static const int _values[] = {1, 2, 4, 8}; static const char * const _names[] = {"North", "South", "East", "West"}; int _value; // ...Функции, использующие вышеприведённое объявление... };
А затем перейдет к преобразованию: найдет индекс значения или строковой в _values
или _names
и вернет его соответствующее значение или строковую в другой массив.
Массив значений
_values
генерируется путём обращения к константам внутреннего перечисления _Enum
. Эта часть макроса выглядит так:
static const int _values[] = {__VA_ARGS__};
Она трансформируется в:
static const int _values[] = {North = 1, South = 2, East = 4, West = 8};
Это почти правильное объявление массива. Проблема заключается в дополнительных инициализаторах вроде «= 1». Для работы с ними Better Enums определяет вспомогательный тип, предназначенный для оператора присваивания, но игнорирует само присваиваемое значение:
template <typename T> struct _eat { T _value; template <typename Any> _eat& operator =(Any value) { return *this; } // Игнорирует аргумент. explicit _eat(T value) : _value(value) { } // Преобразует из T. operator T() const { return _value; } // Преобразует в T. }
Теперь можно включить инициализатор «= 1» в выражение присваивания, не имеющее значения:
static const int _values[] = {(_eat<_Enum>)North = 1, (_eat<_Enum>)South = 2, (_eat<_Enum>)East = 4, (_eat<_Enum>)West = 8};
Массив строковых
Для создания этого массива Better Enums использует (#
) — препроцессорный оператор перевода в строковое (stringization). Он конвертирует __VA_ARGS__
в нечто подобное:
static const char * const _names[] = {"North = 1", "South = 2", "East = 4", "West = 8"};
Теперь мы почти преобразовали имена констант в строковые. Осталось избавиться от инициализаторов. Однако Better Enums этого не делает. Просто при сравнении строковых в массиве _names
он воспринимает символы пробелов и равенства как дополнительные границы строк. Так что при поиске “North = 1
” Better Enums найдёт только “North
”.
Можно ли обойтись без макроса?
Вряд ли. Дело в том, что в С++ оператор (#) — единственный способ преобразования токена исходного кода в строковое. Так что в любой библиотеке, автоматически преобразующей перечисления с рефлексией, приходится использовать как минимум один высокоуровневый макрос.
Прочие соображения
Конечно, полностью рассматривать реализацию макроса было бы гораздо скучнее и сложнее, чем это сделано в данной статье. В основном, сложности возникают из-за поддержки работающих с массивами static
функций constexpr
, из-за особенностей разных компиляторов. Также определённые затруднения могут быть связаны с разложением как можно большей части макроса на шаблоны ради ускорения компиляции (шаблоны не нужно репарсить в ходе создания, а расширения-макросы — нужно).
ссылка на оригинал статьи https://habrahabr.ru/post/277757/
Добавить комментарий