Доступный для поиска объект типа Enum с преобразованием строк и int
вступление
enum
тип в C++ довольно простой; это в основном просто создает кучу значений времени компиляции для меток (возможно, с надлежащей областью видимости с enum class
).
Это очень привлекательно для группировки связанных констант времени компиляции:
enum class Animal{
DOG,
CAT,
COW,
...
};
// ...
Animal myAnimal = Animal::DOG;
Тем не менее, он имеет целый ряд видимых недостатков, в том числе:
- Нет стандартного способа получить количество возможных элементов
- Нет итерации по элементам
- Нет легкой ассоциации enum со строкой
В этом посте я пытаюсь создать тип, который устраняет эти предполагаемые недостатки.
Идеальное решение берет понятие знания констант во время компиляции и их ассоциаций со строками и группирует их вместе в подобный объекту объем, который доступен для поиска как по enum id, так и по имени enum string. Наконец, результирующий тип будет использовать синтаксис, максимально приближенный к синтаксису enum.
В этом посте я сначала опишу, что другие пытались сделать для отдельных частей, а затем пройдусь по двум подходам: один, который выполняет вышеупомянутое, но имеет неопределенное поведение из-за порядка инициализации статических элементов, и другое решение, которое имеет менее симпатичный синтаксис, но нет неопределенного поведения из-за порядка инициализации.
Предыдущая работа
Есть много вопросов по SO о получении количества элементов в перечислении ( 1 2 3) и множество других вопросов в сети, задающих то же самое ( 4 5 6) и т. Д. И общий консенсус заключается в том, что огненный способ сделать это.
Трюк с N-м элементом
Следующий шаблон работает только в том случае, если вы устанавливаете, что значения перечисления являются положительными и увеличиваются:
enum Foo{A=0, B, C, D, FOOCOUNT}; // FOOCOUNT is 4
Но это легко сломать, если вы пытаетесь кодировать какую-то бизнес-логику, которая требует произвольных значений:
enum Foo{A=-1, B=120, C=42, D=6, FOOCOUNT}; // ????
Boost Enum
И поэтому разработчики в Boost попытались решить проблему с Boost.Enum, который использует несколько довольно сложных макросов для расширения до некоторого кода, который по крайней мере даст вам размер.
Итерируемые Перечисления
Было несколько попыток повторяемых перечислений; enum-подобные объекты, которые можно перебирать, теоретически допуская неявное вычисление размера или даже явно в случае [7] ( 7 8 9,...)
Перечисление Enum в String
Попытки реализовать это обычно приводят к свободным плавающим функциям и использованию макросов для их надлежащего вызова. ( 8 9 10)
Это также охватывает поиск перечислений по строке ( 13)
Дополнительные ограничения
Нет макросов
Да, это означает, что нет Boost.Enum или подобный подход
Нужна конвертация en- и enum-int
Довольно уникальная проблема, когда вы начинаете отходить от фактических перечислений;
Должен быть в состоянии найти enum по int (или по строке)
Также проблема, с которой сталкиваются, когда они уходят от фактических перечислений. Список перечислений считается коллекцией, и пользователь хочет запросить его для определенных значений, известных во время компиляции. (Смотрите итерируемые перечисления и преобразования Enum в String)
В этот момент становится совершенно ясно, что мы не можем больше использовать перечисление. Тем не менее, я все еще хотел бы подобный enum-интерфейс для пользователя
Подход
Допустим, я думаю, что я супер умный и понимаю, что если у меня есть какой-то класс A
:
struct A
{
static int myInt;
};
int A::myInt;
Тогда я могу получить доступ myInt
говоря A::myInt
,
Который так же, как я бы получить доступ к enum
:
enum A{myInt};
// ...
// A::myInt
Я говорю себе: хорошо, я знаю все мои значения перечисления заранее, поэтому перечисление в основном так:
struct MyEnum
{
static const int A;
static const int B;
// ...
};
const int MyEnum::A = 0;
const int MyEnum::B = 1;
// ...
Далее я хочу стать более любопытным; давайте обратимся к ограничению, где нам нужно std::string
а также int
преобразования:
struct EnumValue
{
EnumValue(std::string _name): name(std::move(_name)), id(gid){++gid;}
std::string name;
int id;
operator std::string() const
{
return name;
}
operator int() const
{
return id;
}
private:
static int gid;
};
int EnumValue::gid = 0;
И тогда я могу объявить некоторый содержащий класс с static
EnumValue
s:
MyEnum v1
class MyEnum
{
public:
static const EnumValue Alpha;
static const EnumValue Beta;
static const EnumValue Gamma;
};
const EnumValue MyEnum::Alpha = EnumValue("Alpha")
const EnumValue MyEnum::Beta = EnumValue("Beta")
const EnumValue MyEnum::Gamma = EnumValue("Gamma")
Большой! Это решает некоторые из наших ограничений, но как насчет поиска в коллекции? Хм, хорошо, если мы теперь добавим static
контейнер как unordered_map
тогда все становится еще круче! Добавьте в некоторых #define
s, чтобы облегчить опечатки строки, также:
MyEnum v2
#define ALPHA "Alpha"
#define BETA "Beta"
#define GAMMA "Gamma"
// ...
class MyEnum
{
public:
static const EnumValue& Alpha;
static const EnumValue& Beta;
static const EnumValue& Gamma;
static const EnumValue& StringToEnumeration(std::string _in)
{
return enumerations.find(_in)->second;
}
static const EnumValue& IDToEnumeration(int _id)
{
auto iter = std::find_if(enumerations.cbegin(), enumerations.cend(),
[_id](const map_value_type& vt)
{
return vt.second.id == _id;
});
return iter->second;
}
static const size_t size()
{
return enumerations.size();
}
private:
typedef std::unordered_map<std::string, EnumValue> map_type ;
typedef map_type::value_type map_value_type ;
static const map_type enumerations;
};
const std::unordered_map<std::string, EnumValue> MyEnum::enumerations =
{
{ALPHA, EnumValue(ALPHA)},
{BETA, EnumValue(BETA)},
{GAMMA, EnumValue(GAMMA)}
};
const EnumValue& MyEnum::Alpha = enumerations.find(ALPHA)->second;
const EnumValue& MyEnum::Beta = enumerations.find(BETA)->second;
const EnumValue& MyEnum::Gamma = enumerations.find(GAMMA)->second;
Полное рабочее демо ЗДЕСЬ!
Теперь я получаю дополнительное преимущество поиска контейнера перечислений по name
или же id
:
std::cout << MyEnum::StringToEnumeration(ALPHA).id << std::endl; //should give 0
std::cout << MyEnum::IDToEnumeration(0).name << std::endl; //should give "Alpha"
НО
Все это очень неправильно. Мы инициализируем много статических данных. Я имею в виду, что только недавно мы могли заселить map
во время компиляции! ( 11)
Тогда есть проблема фиаско порядка статической инициализации:
Тонкий способ сбить вашу программу.
Статический порядок инициализации фиаско является очень тонким и часто неправильно понимаемым аспектом C++. К сожалению, это очень трудно обнаружить - ошибки часто возникают до того, как начинается main().
Короче говоря, предположим, что у вас есть два статических объекта x и y, которые существуют в отдельных исходных файлах, например, x.cpp и y.cpp. Предположим далее, что инициализация для объекта y (обычно конструктора объекта y) вызывает некоторый метод для объекта x.
Вот и все. Это так просто.
Трагедия в том, что у вас есть 50%-50% шансов умереть. Если модуль компиляции для x.cpp сначала инициализируется, все хорошо. Но если модуль компиляции для y.cpp инициализируется первым, то инициализация y будет выполняться до инициализации x, и вы готовы. Например, конструктор y может вызывать метод объекта x, но объект x еще не создан.
Я слышал, что они нанимают в Макдональдсе. Наслаждайтесь своей новой работой, переворачивая гамбургеры.
Если вы считаете, что играть в "Русскую рулетку" с живыми раундами в половине залов "интересно", вы можете перестать читать здесь. С другой стороны, если вы хотите повысить свои шансы на выживание путем систематического предотвращения бедствий, вы, вероятно, захотите прочитать следующий FAQ.
Примечание: статический порядок инициализации фиаско также может в некоторых случаях применяться к встроенным / внутренним типам.
Который может быть связан с функцией получения, которая инициализирует ваши статические данные и возвращает их ( 12):
Fred& GetFred()
{
static Fred* ans = new Fred();
return *ans;
}
Но если я сделаю это, теперь мне нужно вызвать функцию для инициализации моих статических данных, и я потеряю красивый синтаксис, который вы видите выше!
# Вопросы # Итак, теперь я наконец-то дошел до своих вопросов:
- Будьте честны, насколько плохой подход выше? С точки зрения безопасности порядка инициализации и ремонтопригодности?
- Какие у меня есть альтернативы, которые все еще хорошидля конечного пользователя?
РЕДАКТИРОВАТЬ
Кажется, что комментарии к этой записи указывают на то, что статические функции доступа предпочитают обходить проблему инициализации статического порядка:
public:
typedef std::unordered_map<std::string, EnumValue> map_type ;
typedef map_type::value_type map_value_type ;
static const map_type& Enumerations()
{
static map_type enumerations {
{ALPHA, EnumValue(ALPHA)},
{BETA, EnumValue(BETA)},
{GAMMA, EnumValue(GAMMA)}
};
return enumerations;
}
static const EnumValue& Alpha()
{
return Enumerations().find(ALPHA)->second;
}
static const EnumValue& Beta()
{
return Enumerations().find(BETA)->second;
}
static const EnumValue& Gamma()
{
return Enumerations().find(GAMMA)->second;
}
Полное рабочее демо v2 ЗДЕСЬ
Вопросы
Мои обновленные вопросы следующие:
- Есть ли другой способ обойти проблему инициализации статического порядка?
Есть ли способ, возможно, использовать только функцию доступа для инициализации
unordered_map
, но все же (безопасно) быть в состоянии получить доступ к значениям "enum" с синтаксисом, подобным enum? например:MyEnum::Enumerations()::Alpha
или же
MyEnum::Alpha
Вместо того, что у меня сейчас есть:
MyEnum::Alpha()
Что касается щедрости:
Я полагаю, что ответ на этот вопрос также решит проблемы, связанные с перечислениями, которые я разработал в посте (Enum находится в кавычках, потому что результирующий тип не будет перечислением, но нам нужно поведение, подобное перечислению):
- получить размер "enum"
- преобразование строки в enum
- поиск "enum".
В частности, если бы мы могли сделать то, что я уже сделал, но каким-то образом выполнить синтаксис, подобный enum, при применении статического порядка инициализации, я думаю, что это будет приемлемо
2 ответа
Иногда, когда вы хотите сделать что-то, что не поддерживается языком, вы должны выглядеть внешне по отношению к языку для его поддержки. В этом случае генерация кода кажется лучшим вариантом.
Начните с файла с вашим перечислением. Я выберу XML совершенно произвольно, но на самом деле подойдет любой разумный формат:
<enum name="MyEnum">
<item name="ALPHA" />
<item name="BETA" />
<item name="GAMMA" />
</enum>
Достаточно легко добавить туда любые необязательные поля, которые вам нужны (вам нужно value
? Если enum
быть незаданным? Есть указанный тип?).
Затем вы пишете генератор кода на выбранном вами языке, который превращает этот файл в файл заголовка (или заголовка / источника) C++ в виде:
enum class MyEnum {
ALPHA,
BETA,
GAMMA,
};
std::string to_string(MyEnum e) {
switch (e) {
case MyEnum::ALPHA: return "ALPHA";
case MyEnum::BETA: return "BETA";
case MyEnum::GAMMA: return "GAMMA";
}
}
MyEnum to_enum(const std::string& s) {
static std::unordered_map<std::string, MyEnum> m{
{"ALPHA", MyEnum::ALPHA},
...
};
auto it = m.find(s);
if (it != m.end()) {
return it->second;
}
else {
/* up to you */
}
}
Преимущество подхода генерации кода состоит в том, что он легко генерирует любой произвольный сложный код, который вы хотите для своих перечислений. По сути, просто обойдите все проблемы, которые у вас сейчас есть.
Я обычно предпочитаю не макросовый код, но в этом случае я не вижу, что не так с макросами.
ИМХО, для этой задачи лучше подходят макросы, так как их проще и короче писать и читать, и то же самое касается сгенерированного кода. Простота - это сама по себе цель.
Эти 2 макро вызова:
#define Animal_Members(LAMBDA) \
LAMBDA(DOG) \
LAMBDA(CAT) \
LAMBDA(COW) \
CREATE_ENUM(Animal,None);
Создайте это:
struct Animal {
enum Id {
None,
DOG,
CAT,
COW
};
static Id fromString( const char* s ) {
if( !s ) return None;
if( strcmp(s,"DOG")==0 ) return DOG;
if( strcmp(s,"CAT")==0 ) return CAT;
if( strcmp(s,"COW")==0 ) return COW;
return None;
}
static const char* toString( Id id ) {
switch( id ) {
case DOG: return "DOG";
case CAT: return "CAT";
case COW: return "COW";
default: return nullptr;
}
}
static size_t count() {
static Id all[] = { None, DOG, CAT, COW };
return sizeof(all) / sizeof(Id);
}
};
Вы можете обернуть их в один макрос, используя BOOST_PP, и иметь последовательность для членов. Это сделало бы его намного менее читабельным, хотя.
Вы можете легко изменить его в соответствии со своими предпочтениями возвращаемых значений по умолчанию или вообще удалить значения по умолчанию, добавить конкретное значение члена и имя строки и т. Д.
Там нет свободных функций, нет порядка ада инициализации, и только немного макроса, который очень похож на конечный результат:
#define ENUM_MEMBER(MEMBER) \
, MEMBER
#define ENUM_FROM_STRING(MEMBER) \
if( strcmp(s,#MEMBER)==0 ) return MEMBER;
#define ENUM_TO_STRING(MEMBER) \
case MEMBER: return #MEMBER;
#define CREATE_ENUM_1(NAME,MACRO,DEFAULT) \
struct NAME { \
enum Id { \
DEFAULT \
MACRO(ENUM_MEMBER) \
}; \
static Id fromString( const char* s ) { \
if( !s ) return DEFAULT; \
MACRO(ENUM_FROM_STRING) \
return DEFAULT; \
} \
static const char* toString( Id id ) { \
switch( id ) { \
MACRO(ENUM_TO_STRING) \
default: return nullptr; \
} \
} \
static size_t count() { \
static Id all[] = { DEFAULT \
MACRO(ENUM_MEMBER) }; \
return sizeof(all) / sizeof(Id); \
} \
};
#define CREATE_ENUM_2(NAME,DEFAULT) \
CREATE_ENUM_1(NAME,NAME##_Members,DEFAULT)
#define CREATE_ENUM(NAME,DEFAULT) \
CREATE_ENUM_2(NAME,DEFAULT)
Надеюсь это поможет.