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

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

Комментарии (7)


  1. tangro
    24.02.2016 11:21
    +4

    Перевод собственного ответа со Stackoverflow? :)


    1. vladon
      24.02.2016 11:29
      +3

      Ну значит плюсанём и там :)


  1. NeoCode
    24.02.2016 12:06
    +3

    На самом деле есть много способов. Один из них не так давно рассматривался на Хабре.
    Но самое удивительное — есть способы организации полной рефлексии не только на современном С++, но и на чистом Си, причем времен создания языка (70-х годов!).

    Есть способ на boost::preprocessor, который в общем не зависит от того, Си это или С++. Достаточно навороченный способ, требующий большой отдельной статьи.

    А есть еще один способ, невероятно простой и прозрачный — основанный на двойном include заголовочого файла; элементы синтаксической конструкции (не только перечисления, но и например структуры) объявляются с помощью специальных макросов, которые в зависимости от некоторой макропеременной раскрываются или в обычные элементы (имя перечисления, идентификаторы элементов и т.д.), или в элементы статического массива данных рефлекси.

    #ifdef GENERATE_REFLECTION
    #define ENUM_ITEM(item, data) data, 
    #else
    #define ENUM_ITEM(item, data) item, 
    #endif

    Ну а дальше очевидно: везде используется обычное подключение заголовочного файла с перечислением, и только в одном единственном месте (точке инстанцирования, которая должна быть .c или .cpp файлом) предварительно объявляется макрос и создается статический объект рефлексии

    #include "my_enum.h"
    #define GENERATE_REFLECTION
    #include "my_enum.h"
    #undef GENERATE_REFLECTION


  1. Gorthauer87
    24.02.2016 12:49

    По-моему круче всего было бы просто сделать плагин к clang'у и добавить какой-нибудь атрибут. Была ещё экспериментальная версия Qtшного moc'а в виде плагина. Вообще идеально выглядело бы что-то такое:

    [[display(prefix="my_")]]
    enum { first_item, second_item }


    1. NeoCode
      24.02.2016 13:36
      +2

      Круче всего было бы если бы рефлексию добавили в C++17


      1. Gorthauer87
        24.02.2016 13:57
        +1

        Но почему-то не заметно, чтобы это произошло.


        1. NeoCode
          24.02.2016 19:16

          Ну пока еще не произошло, но все в наших руках.