[Из песочницы] Добавляем рефлексию для перечислений (enum) в C++
Недавно в нашем проекте возникла необходимость программно получать информацию о перечислениях (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
inline const EnumReflector& EnumReflector::For(EnumType val)
{
return _detail_reflector_(val);
}
Хитрость тут только в том, что используется ADL для поиска функции _detail_reflector_ по типу аргумента. Именно благодаря ADL мы можем получить информацию для перечислений вне зависимости от их класса или пространства имен.
Но вернемся в функцию _detail_reflector_.
Для обеспечения атомарности, вся инициализация статического объекта EnumReflector происходит внутри безымянной лямбда-функции. Рассмотрим её поподробнее.
Сначала в ней объявляется статическая переменная-счетчик sval. Статическая она потому, что нам потребуется обращаться к ней из локального класса val_t, определенного далее. Не имея дополнительного состояния, локальный класс, очевидно, может обращаться только к статическим переменным внешнего блока. В переменной sval будет храниться следующее значение для константы. Следующей строчкой мы инициализируем её в 0.
На первый взгляд, это бессмысленное действие: изначально статическая переменная и так инициализирована в 0, а этот код будет выполнятся всего лишь один раз. Однако, проведя некоторые тесты, я заметил, что компиляторы гораздо лучше оптимизируют этот код, если мы явно сбросим значение перед использованием. Это, вероятно, обусловлено тем, что компилятору в этом случае не нужно исходить из возможных предыдущих значений sval
Далее определяется тип 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 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.: Приветствуются предложения по улучшению данного способа.