Доступный для поиска объект типа 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;

И тогда я могу объявить некоторый содержащий класс с staticEnumValues:

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тогда все становится еще круче! Добавьте в некоторых #defines, чтобы облегчить опечатки строки, также:


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)

Надеюсь это поможет.

Другие вопросы по тегам