Недавно в нашем проекте возникла необходимость программно получать информацию о перечислениях (enum), например, имена констант в виде строк, а также общий список всех имеющихся в enum-е констант.
enum Suit { Spades, Hearts, Diamonds, Clubs };
Обычно решение данной задачи базируется на дублировании значений, например, внутри switch-а:
switch(value) { case Spades: return "Spades"; case Hearts: return "Hearts"; case Diamonds: return "Diamonds"; case Clubs: return "Clubs"; default: return "" };
И возможно, для небольших перечислений такое решение действительно является приемлемым, однако если значений много, и особенно, если они время от времени меняются, то рано или поздно разработчик может забыть дописать или изменить соответствующие строки в switch. Сюда прибавляются и другие очевидные минусы, например сам факт необходимости дублирования значений уже вызывает у меня некоторое недовольство.
Поэтому я постарался найти путь, который вообще не требовал бы дублирования, но при этом полностью справлялся бы с поставленной задачей. Думаю, у меня получилось.
Далее в статье я опишу способ, позволяющий организовать рефлексию для enum-ов. Кому интересно — добро пожаловать под кат.
Зачем это вообще нужно
Полезных применений может быть много. Одно из них — сериализация значений, например в JSON.
Также это может пригодиться для взаимодействия кода на C++ со скриптовыми языками (например, Lua).
Требования
Раз мы хотим избежать дублирования констант в коде, то нам нужно как-то сохранить информацию о всех значениях прямо в месте определения перечисления. Как вы уже, возможно, догадались, для этой цели придется использовать макрос. Учитывая это, можно выделить некоторые дополнительные требования:
- Синтаксис макроса для описания перечисления должен быть совместим с обычным enum
- Само перечисление (как тип) не должно отличаться от обычного enum (в т. ч. должно быть возможно потом использовать typedef)
- При описании значений должны сохраняться те же возможности, что и в обычном перечислении
Иными словами, мы должны быть способны без труда обернуть уже существующее перечисление в наш макрос, после чего нам сразу будет (программно) доступна информация о нем.
Обязательным условием также является полная портируемость.
Результат
Сначала, привожу краткое описание того, что получилось. Ниже в статье будет описание деталей реализации.
Для добавления рефлексии, перечисление вместо ключевого слова enum следует объявлять с помощью макроса Z_ENUM. Например, для enum CardSuit из начала статьи, это выглядит следующим образом:
Z_ENUM( CardSuit, Spades, Hearts, Diamonds, Clubs )
После этого в любом месте можно по типу перечисления получить ссылку на объект EnumReflector, который хранит о нем информацию:
auto& reflector = EnumReflector::For< CardSuit >();
Далее всё просто:
reflector.EnumName(); // == "CardSuit" reflector.Find("Diamonds").Value(); // == 2 reflector.Count(); // == 4 reflector[1].Name(); // == "Hearts"
Следующий пример показывает более сложное перечисление:
class SomeClass { public: static const int Constant = 100; Z_ENUM( TasteFlags, None = 0, Salted = 1 << 0, Sour = 1 << 1, Sweet = 1 << 2, SourSweet = (Sour | Sweet), Other = Constant, Last ) };
На этот раз получим всю имеющуюся информацию:
auto& reflector = EnumReflector::For< SomeClass::TasteFlags >(); cout << "Enum " << reflector.EnumName() << endl; for (auto& val : reflector) { cout << "Value " << val.Name() << " = " << val.Value() << endl; }
Вывод:
Enum TasteFlags Value None = 0 Value Salted = 1 Value Sour = 2 Value Sweet = 4 Value SourSweet = 6 Value Other = 100 Value Last = 101
Особенности
- В отличие от обычного enum, после последнего значения не допускается запятая
- Если перечисление объявляется вне класса (на уровне namespace), то вместо Z_ENUM следует использовать полностью аналогичный ему Z_ENUM_NS
Причины появления этих двух пунктов рассматриваются в следующей секции.
Детали реализации
Итак, самое интересное.
Примечание: код, приводимый здесь упрощен в целях повышения читаемости. Полную версию вы можете найти на гитхабе, ссылка в конце статьи.
Макрос Z_ENUM:
#define Z_ENUM(enumName, ...)\ enum enumName : int \ { \ __VA_ARGS__ \ }; \ friend const ::EnumReflector& _detail_reflector_(enumName) \ { \ static const ::EnumReflector reflector( []{ \ static int sval; \ sval = 0; \ struct val_t \ { \ val_t(const val_t& rhs) : _val(rhs) { sval = _val + 1; } \ val_t(int val) : _val(val) { sval = _val + 1; } \ val_t() : _val(sval){ sval = _val + 1; } \ \ val_t& operator=(const val_t&) { return *this; } \ val_t& operator=(int) { return *this; } \ operator int() const { return _val; } \ int _val; \ } __VA_ARGS__; \ const int vals[] = { __VA_ARGS__ }; \ return ::EnumReflector( vals, sizeof(vals)/sizeof(int), \ #enumName, Z_ENUM_DETAIL_STR((__VA_ARGS__)) ); \ }() ); \ return reflector; \ } #define Z_ENUM_DETAIL_STR(x) #x
enum TasteFlags:int { None = 0, Salted = 1 << 0, Sour = 1 << 1, Sweet = 1 << 2, SourSweet = (Sour | Sweet), Other = Constant, Last }; friend const ::EnumReflector& _detail_reflector_(TasteFlags) { static const ::EnumReflector reflector( [] { static int sval; sval = 0; struct val_t { val_t(const val_t& rhs) : _val(rhs) { sval = _val + 1; } val_t(int val) : _val(val) { sval = _val + 1; } val_t() : _val(sval){ sval = _val + 1; } val_t& operator=(const val_t&) { return *this; } val_t& operator=(int) { return *this; } operator int() const { return _val; } int _val; } None = 0, Salted = 1 << 0, Sour = 1 << 1, Sweet = 1 << 2, SourSweet = (Sour | Sweet), Other = Constant, Last; const int vals[] = { None = 0, Salted = 1 << 0, Sour = 1 << 1, Sweet = 1 << 2, SourSweet = (Sour | Sweet), Other = Constant, Last }; return ::EnumReflector( vals, sizeof(vals)/sizeof(int), "TasteFlags", "( None = 0, Salted = 1 << 0, Sour = 1 << 1, Sweet = 1 << 2, SourSweet = (Sour | Sweet), Other = Constant, Last)" ); }()); return reflector; }
Рассмотрим его по частям:
В начале Z_ENUM раскрывается в обычный enum. Можно заметить, что явно указывается нижележащий тип данных — int. Так сделано только потому, что в EnumReflector сейчас значения хранятся с типом int. При необходимости int можно заменить на более большой тип.
После объявляется friend-функция _detail_reflector_. Она принимает значение типа нашего перечисления и возвращает ссылку на объект EnumReflector, который на самом деле является статическим объектом, объявленным внутри нее.
Немного забегая вперед, приведу функцию EnumReflector::For, которая служит внешним интерфейсом для получения объекта EnumReflector:
template<typename EnumType> inline const EnumReflector& EnumReflector::For(EnumType val) { return _detail_reflector_(val); }
Хитрость тут только в том, что используется ADL для поиска функции _detail_reflector_ по типу аргумента. Именно благодаря ADL мы можем получить информацию для перечислений вне зависимости от их класса или пространства имен.
Но вернемся в функцию _detail_reflector_.
Для обеспечения атомарности, вся инициализация статического объекта EnumReflector происходит внутри безымянной лямбда-функции. Рассмотрим её поподробнее.
Сначала в ней объявляется статическая переменная-счетчик sval. Статическая она потому, что нам потребуется обращаться к ней из локального класса val_t, определенного далее. Не имея дополнительного состояния, локальный класс, очевидно, может обращаться только к статическим переменным внешнего блока. В переменной sval будет храниться следующее значение для константы. Следующей строчкой мы инициализируем её в 0.
Далее определяется тип val_t. После описания типа еще раз раскрывается __VA_ARGS__ (значения нашего перечисления). То есть мы определяем локальные переменные типа val_t — и их количество соответствует количеству значений в перечислении, а имена соответствуют самим константам (они перекрывают собой настоящие константы определенного до этого enum-а). Для того, чтобы инициализация этих переменных правильно работала, у типа val_t есть три конструктора. Каждый из них дополнительно устанавливает sval в следующее после себя значение, на случай если у следующей константы нет специально заданного значения.
Именно в этом месте, если после последнего значения имеется запятая — возникнет синтаксическая ошибка.
После, нам необходимо «перегнать» значения из переменных в массива типа int. Благодаря оператору преобразования в int у val_t это сделать довольно просто — мы можем в качестве инициализаторов массива сразу использовать наши переменные типа val_t, просто еще раз раскрыв __VA_ARGS__. Поскольку при таком раскрытии могут присутствовать присваивания, то мы добавляем в val_t два оператора присваивания, которые ничего не делают — таким образом мы полностью игнорируем присваивания.
Теперь, когда у нас есть массив всех значений и известно их количество, нужно получить названия констант в виде строк. Для этого все значения оборачиваются в строку вида "(__VA_ARGS__)". Эта строка, наряду с указателем на массив и количеством элементов, передается в конструктор EnumReflector. Ему осталось только распарсить строку, выделив из нее имена констант, и сохранить все значения.
Сам парсер для быстродействия организован в виде простого конечного автомата.
struct EnumReflector::Private { struct Enumerator { std::string name; int value; }; std::vector<Enumerator> values; std::string enumName; }; static bool IsIdentChar(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '_'); } EnumReflector::EnumReflector(const int* vals, int count, const char* name, const char* body) : _data(new Private) { _data->enumName = name; _data->values.resize(count); enum states { state_start, // Before identifier state_ident, // In identifier state_skip, // Looking for separator comma } state = state_start; assert(*body == '('); ++body; const char* ident_start = nullptr; int value_index = 0; int level = 0; for (;;) { assert(*body); switch (state) { case state_start: if (IsIdentChar(*body)) { state = state_ident; ident_start = body; } ++body; break; case state_ident: if (!IsIdentChar(*body)) { state = state_skip; assert(value_index < count); _data->values[value_index].name = std::string(ident_start, body - ident_start); _data->values[value_index].value = vals[value_index]; ++value_index; } else { ++body; } break; case state_skip: if (*body == '(') { ++level; } else if (*body == ')') { if (level == 0) { assert(value_index == count); return; } --level; } else if (level == 0 && *body == ',') { state = state_start; } ++body; } } }
Мы просто идем по строке, сохраняя идентификаторы (названия констант). После очередного идентификатора, мы ищем начало следующего идентификатора, и так далее. В конце имеем готовую структуру данных, содержащую всю информацию о перечислении.
Остальная часть реализации класса EnumReflector служит для получения этой информации и на мой взгляд, не представляет особого интереса для данной статьи. Напоминаю, в конце есть ссылка на полную версию.
При объявлении перечисления вне класса функция _detail_reflector_ должна быть объявлена не как friend, а как inline. Отсюда необходимость в отдельном макросе Z_ENUM_NS. Чтобы случайно не использовать Z_ENUM_NS в теле класса, в нем также присутствует пустой блок extern «C» {} (напоминаю, его использование в теле класса не допускается стандартом, так что получим ошибку компиляции).
Также, во избежание возникновения коллизий имён с константами, в полной версии все идентификаторы внутри функции _detail_reflector_ имеют префикс _detail_.
Что можно улучшить
Можно попробовать выполнять парсинг для получения названий прямо на этапе компиляции, используя user-defined литералы для строк и constexpr функции из C++14.
Также было бы неплохо избавиться от необходимости в двух разных макросах для определения перечисления в классе и вне класса, но пока что я не нашел способа это сделать, не сломав при этом поиск ADL.
Ссылки
Полная версия кода из статьи: github.com.
Argument-Dependent Lookup: cppreference.com.
На этом всё. Надеюсь, статья получилась интересной.
P.S.: Приветствуются предложения по улучшению данного способа.
ссылка на оригинал статьи https://habrahabr.ru/post/276763/
Добавить комментарий