[Перевод] Удобное преобразование перечислений (enum) в строковые в С++
У перечислений есть множество способов применения в разработке. Например, при создании игр они используются для программирования состояний персонажа или возможных направлений движения:
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
struct _eat {
T _value;
template
_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
, из-за особенностей разных компиляторов. Также определённые затруднения могут быть связаны с разложением как можно большей части макроса на шаблоны ради ускорения компиляции (шаблоны не нужно репарсить в ходе создания, а расширения-макросы — нужно).