Поисковый объект, похожий на enum, с преобразованием строки и int

Введение

Тип 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"
  • поисковое "перечисление".

В частности, если мы могли бы сделать то, что я уже сделал, но каким-то образом выполняем синтаксис, похожий на перечисление, при соблюдении статического порядка инициализации, я думаю, что это было бы приемлемо

Ответы

Ответ 1

Иногда, когда вы хотите сделать что-то, что не поддерживается языком, вы должны смотреть на внешний язык, чтобы его поддерживать. В этом случае генерация кода кажется лучшим вариантом.

Начните с файла с перечислением. Я выберу XML полностью произвольно, но на самом деле любой разумный формат в порядке:

<enum name="MyEnum">
    <item name="ALPHA" />
    <item name="BETA" />
    <item name="GAMMA" />
</enum>

Достаточно легко добавить любые необязательные поля, которые вам нужны (вам нужен value? Если enum не обладали ли они правами? Имеют ли указанный тип?).

Затем вы пишете генератор кода на выбранном вами языке, который превращает этот файл в файл заголовка С++ (или заголовок/источник) a la:

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

Я обычно предпочитаю не-макрокод, но в этом случае я не вижу, что неправильно с макросами.
IMHO, для этой задачи макросы намного лучше подходят, так как они проще и короче, чтобы писать и читать, и то же самое относится к сгенерированному коду. Простота - это цель по своему усмотрению.

Эти два вызова макроса:

#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)

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