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.: Приветствуются предложения по улучшению данного способа.
Комментарии (14)
isotoxin
08.02.2016 13:47+2Я использую метод на макросах. По сравнению с предложенным в статье, этот метод не имеет оверхеда, а также дает дополнительные возможности.
// объявляем список #define MY_ENUM_DESC EITM( vasya ) EITM( petya ) EITM( vova ) // генерируем enum enum my_enum { #define EITM(itm) itm, MY_ENUM_DESC #undef EITM my_enum_max }; // функция для получения строки const char * my_enum_s(my_enum e) { static const char * tbl[] = { #define EITM(itm) #itm, MY_ENUM_DESC #undef EITM "" }; return tbl[e]; }
Кроме того, по списку можно делать практически все что угодно: объявлять другие типы, создавать функции/классы, делать сериализацию и т.п.
Недостаток — трудночитаемый код. Но мне кажется, это приемлемая цена за надежность.semenyakinVS
08.02.2016 13:55+1Метод интересный, но очень уж громоздкий. Особенно с учётом того, что нумераторов в проекте могут быть десятки. И вот это:
Недостаток — трудночитаемый код. Но мне кажется, это приемлемая цена за надежность
на мой взгляд, большой недостаток. Будет море копи-пасты — ведь подобные группы из макроса, нумератора и функции придётся копи-пастить каждый тип (засунуть под один макрос эти три штуки я особо не представляю как).isotoxin
08.02.2016 14:16Если много enum-ов, требующих рефлексии, то да, громоздко.
Метод на макросах лучше применять не часто. По моему опыту, даже крупные проекты имеют не более 5 таких списков.
Кстати, на макросах — это даже не рефлексия. Это больше похоже на мета программирование. enum — это порождение списка и лишь малая часть возможностей.
Например, в своем проекте я сделал список таблиц, которые читаются из sqlite базы. Для каждой таблицы создаются свои классы обработки (по списку инстанцируются шаблоны), всего 8 мест генерации. Добавляя в список новую таблицу, я экономлю кучу времени на написании оснастки этой таблицы — весь код генерирует за меня компилятор.semenyakinVS
08.02.2016 15:33Да. Я работал в компании, где тоже в одном месте такой приём использовался. Сильный механизм, который, тем не менее, стоит использовать не больше нескольких раз в проекте — очень уж громоздкий.
На самом деле, на мой взгляд, подобные ходули — так же как 90% извращений на шаблонно-темплейтной чёрной магии в С++ — возникают потому, что нет нормальной возможности редактирования AST программы на этапе компиляции. Нужен полноценный язык (скорее всего — функциональный) с отладчиком — для выполнения кодогенерации для С++. Задача очень непростая (прежде всего потому, что требуется сохранить читабельность runtime-кода), но, на мой взгляд, рано или поздно придётся это сделать — иначе язык утонет в разнотипных шаблонных конструкциях.
О необходимости возможности редактирования AST говорят такие ребята, как Эрик Ниблер (один из экспертов в С++, я как раз тут эту мысль встретил и заболел ею). Эту штуку я обсуждал ещё позже с ребятами вот тут — как продолжение дискуссии…
Вы не встречали нигде подобных обсуждений? Интересно было бы глянуть что ещё люди думают по этому поводу.
degs
08.02.2016 22:21+1Статья безусловно получилась. Даже независимо от практического результата вышло очень изящно, просто приятно почитать.
Lol4t0
08.02.2016 23:30Я так и не понял, зачем нужен
val_t
и пляски вокруг него, при том что и без этого все отлично работает ( о чем я писал еще 4 года назад)
std::vector<std::string> parse(const char* args); template<typename T, typename ...Ts> std::map<T, std::string> make_map(const char* text, Ts... args) { std::vector<T> keys{args...}; std::vector<std::string> vals = parse(text); auto k = keys.cbegin(); auto v = vals.cbegin(); std::map<T, std::string> r; for (; k != keys.cend(); k++, v++) { r.emplace(*k, *v); } return r; } #define ENUM(name, ...) enum name { __VA_ARGS__ }; static std::string to_string(const name v) { static std::map<name, std::string> m {make_map<name>(#__VA_ARGS__, __VA_ARGS__)}; return m.at(v); }
thedsi666
09.02.2016 00:12В вашей версии нельзя присваивать произвольные значения константам перечисления.
Lol4t0
09.02.2016 00:13Почему же?
thedsi666
09.02.2016 00:24Допустим, если я допишу A=8 в ваш пример с ideone, то получаем ошибку компиляции:
prog.cpp: In static member function 'static std::string X::to_string(X::Y)': prog.cpp:60:13: error: lvalue required as left operand of assignment ENUM(Y,A=8,B,C) ^
deema35
09.02.2016 02:34Ещё есть способ, вместо enuma использовать структуру со статическими членами:
struct Nums { static const std:string One; static const std:string Two; }; const static const std:string Nums:One = "One"; const static const std:string Nums:Two = "Two";
Вызов не сильно отличается от enuma Nums:One, но вместо цифры мы передадим строку.
newnon
Я писал очень похожую статью habrahabr.ru/post/236403 но у моего метода тоже есть проблемы
Вот еще одна реализация github.com/aantron/better-enums там ограничение только на включение внутрь класса. тем не менее очень хорошо сделана в том числе и compile time преобразования.