[Перевод] Удобное преобразование перечислений (enum) в строковые в С++

78b169bb1e7048bbbf902bbaad599a9b.pngУ перечислений есть множество способов применения в разработке. Например, при создании игр они используются для программирования состояний персонажа или возможных направлений движения:

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, из-за особенностей разных компиляторов. Также определённые затруднения могут быть связаны с разложением как можно большей части макроса на шаблоны ради ускорения компиляции (шаблоны не нужно репарсить в ходе создания, а расширения-макросы — нужно).

© Habrahabr.ru