Введение
Тип enum
на С++ довольно простой; он в основном просто создает кучу значений времени компиляции для меток (возможно, с правильной областью определения с помощью enum class
).
Это очень привлекательно для группировки связанных констант времени компиляции вместе:
enum class Animal{
DOG,
CAT,
COW,
...
};
// ...
Animal myAnimal = Animal::DOG;
Однако он имеет множество предполагаемых недостатков, в том числе:
- Нет стандартного способа получения количества возможных элементов
- Без итераций над элементами
- Легкая ассоциация перечисления со строкой
В этом посте я пытаюсь создать тип, который учитывает эти предполагаемые недостатки.
Идеальное решение принимает понятие знания времени компиляции констант и их ассоциаций со строками и объединяет их вместе в объект, подобный переименованию, который можно искать как с перечислением, так и с именем перечисления enum. Наконец, полученный тип использовал бы синтаксис, максимально приближенный к синтаксису enum.
В этом посте я расскажу о том, что другие пытались сделать для отдельных частей, а затем пройдут два подхода, один из которых выполняет вышеупомянутое, но имеет поведение undefined из-за порядка инициализации статических элементов и другое решение который имеет менее симпатичный синтаксис, но не поведение undefined из-за порядка инициализации.
Предварительная работа
Есть много вопросов о SO о получении количества элементов в перечислении (1 2 3) и множество других вопросов в Интернете, запрашивающих одно и то же (4 5 6) и т.д. И общий консенсус в том, что нет уверенного способа сделать это.
Тройка элемента N'th
Следующий шаблон работает только при условии, что значения перечисления положительны и увеличиваются:
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 или аналогичный подход
-
Требуется int- > Enum и Enum-int Conversion
довольно уникальная проблема, когда вы начинаете отходить от реальных перечислений;
-
Нужно иметь возможность найти enum с помощью int (или строки)
Также возникает проблема, когда они удаляются от фактических перечислений. Список перечислений считается коллекцией, и пользователь хочет допросить его для определенных значений, известных в момент компиляции. (Смотрите итерационные перечисления и преобразование Enum в String)
В этот момент становится довольно ясно, что мы больше не можем использовать перечисление. Тем не менее, мне все равно нужен интерфейс, подобный enum для пользователя.
Подход
Скажем, я считаю, что я супер умный и понимаю, что если у меня есть класс A
:
struct A
{
static int myInt;
};
int A::myInt;
Затем я могу получить доступ к myInt
, сказав A::myInt
.
Точно так же я получаю доступ к enum
:
enum A{myInt};
// ...
// A::myInt
Я говорю себе: хорошо знаю все мои значения enum загодя, поэтому перечисление в основном выглядит следующим образом:
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")
Отлично! Это решает некоторые из наших ограничений, но как насчет поиска в коллекции? Hm, хорошо, если теперь добавить контейнер static
, например unordered_map
, тогда все становится еще круче! В некоторых #define
вы можете сбросить также опечатки строк:
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)
Затем возникает проблема статично-инициализационного порядка фиаско:
Тонкий способ свернуть вашу программу.
Фиксация порядка статической инициализации очень тонкая и обычно непонятый аспект С++. К сожалению, его очень трудно обнаружить - ошибки часто возникают до начала main().
Короче говоря, предположим, что у вас есть два статических объекта x и y, которые существуют в отдельные исходные файлы, скажем, x.cpp и y.cpp. Предположим далее, что инициализация для объекта y (обычно это конструктор y-объектов) вызывает некоторый метод для объекта x.
Вот оно. Это просто.
Трагедия заключается в том, что у вас есть 50% -50% шанс умереть. Если блок компиляции для x.cpp сначала инициализируется, все Что ж. Но если сначала выполнить инициализацию единицы компиляции для y.cpp, то инициализация ys будет запущена до инициализации xs и ты тост. Например, конструктор ys может вызывать метод на x объект, но объект x еще не создан.
Я слышал, как они нанимают в McDonalds. Наслаждайтесь новой работой гамбургеры.
Если вы думаете, что его "захватывающий" играет в русскую рулетку с живыми раундами в половине палат вы можете перестать читать здесь. С другой стороны, если вам нравится улучшать свои шансы на выживание, предотвращая стихийные бедствия систематически, вы, вероятно, захотите прочитать следующий FAQ.
Примечание. Фиаско порядка статического инициализации также может, в некоторых случаях, применяются к встроенным/внутренним типам.
Который может быть опосредован с помощью функции getter, которая инициализирует ваши статические данные и возвращает их (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 ЗДЕСЬ
Вопросы
Мои следующие вопросы:
- Есть ли другой путь вокруг проблемы инициализации статического порядка?
-
Есть ли способ использовать функцию accessor только для инициализации
unordered_map
, но все же (безопасно) иметь возможность доступа к значениям "enum" с синтаксисом типа enum? например:.MyEnum::Enumerations()::Alpha
или
MyEnum::Alpha
Вместо того, что у меня есть:
MyEnum::Alpha()
Что касается награды:
Я считаю, что ответ на этот вопрос также решит проблемы, связанные с перечислениями, которые я разработал в сообщении (Enum в кавычках, потому что результирующий тип не будет перечислением, но мы хотим enum-like поведение):
- получение размера "перечисления"
- строка для преобразования "enum"
- поисковое "перечисление".
В частности, если мы могли бы сделать то, что я уже сделал, но каким-то образом выполняем синтаксис, похожий на перечисление, при соблюдении статического порядка инициализации, я думаю, что это было бы приемлемо