C++ enum <-> string? Легко

    Вот, скажем, один из самых популярных примеров. Можно сказать, классических. Сериализуются данные в, скажем, json. В структуре есть enum-поле, которое хочется сохранять в текстовом виде (а не числом). Всё. Стоп. Простого способа решить эту элементарную задачу на C++ не существует. (c)

    ... Но очень хочется.

    За последний год я видел, как чуть ли не в каждом проекте разработчик предлагал своё видение этой проблемы. И везде было дублирование кода, везде какие-то костыли, и "тонкости". Да что уж там, мне самому приходится время от времени возвращаться к этой теме. Хватит. Решил раз и навсегда закрыть вопрос, по крайней мере для себя.

    Код далёк от совершенства (надеюсь, анонимус поправит), но свою задачу выполняет. Может кому и пригодится:

    Реализация
    // enum_string.h
    #pragma once
    
    #define DECLARE_ENUM(T, values...)                                    \
      enum class T { values, MAX };                                       \
      char enum_##T##_base[sizeof(#values)] = #values;                    \
      const char* T##_tokens[static_cast<__underlying_type(T)>(T::MAX)];  \
      const char* const* T##_tmp_ptr = tokenize_enum_string(              \
          const_cast<char*>(enum_##T##_base), sizeof(#values), T##_tokens,\
          static_cast<__underlying_type(T)>(T::MAX));
    
    #define enum_to_string(T, value) \
      (T##_tokens[static_cast<__underlying_type(T)>(value)])
    
    static const char* const* tokenize_enum_string(char* base,
                                                   int length,
                                                   const char* tokens[],
                                                   int size) {
      int count = 0;
      tokens[count++] = base;
      for (int i = 1; i < length; ++i) {
        if (base[i] == ',') {
          base[i] = '\0';
    
          if (count == size) {
            return tokens;
          }
    
          do {
            if (++i == length) {
              return tokens;
            }
    
          } while (' ' == base[i]);
    
          tokens[count++] = base + i;
        }
      }
    
      return tokens;
    }
    
    static bool string_equals(const char* a, const char* b) {
      int i = 0;
      for (; a[i] && b[i]; ++i) {
        if (a[i] != b[i]) {
          return false;
        }
      }
    
      return (a[i] == b[i]);
    }
    
    static int string_to_enum_int(const char* const tokens[], int max,
                                  const char* value) {
      for (int i = 0; i < max; ++i) {
        if (string_equals(tokens[i], value)) {
          return i;
        }
      }
    
      return max;
    }
    #define string_to_enum(T, value)     \
      static_cast<T>(string_to_enum_int( \
          T##_tokens, static_cast<__underlying_type(T)>(T::MAX), value))
    

    Работу со строками можете без проблем заменить на ваши любимые библиотеки, большинство кода здесь - это как раз парсинг строки (уж очень хотелось обойтись без STL).

    Главная идея была в том, чтобы гарантировать биективность множества enum и его строкового эквивалента, а также сделать реализацию универсальной по количеству элементов (до свидания, вырвиглазный хардкодный макрос _NARG). Ну и, чтобы использование было максимально няшным.

    пример использования
    // main.cpp
    #include <iostream>
    
    #include "enum_string.h"
    
    DECLARE_ENUM(LogLevel,  // enum class LogLevel
                 Alert,     // LogLevel::Alert
                 Critical,  // LogLevel::Critical
                 Error,     // LogLevel::Error
                 Warning,   // LogLevel::Warning
                 Notice,    // LogLevel::Notice
                 Info,      // LogLevel::Info
                 Debug      // LogLevel::Debug
                 );
    
    int main() {
      // serialize
      LogLevel a = LogLevel::Critical;
      std::cout << enum_to_string(LogLevel, a) << std::endl;
    
      // deserialize
      switch (string_to_enum(LogLevel, "Notice")) {
        case LogLevel::Alert: {
          std::cout << "ALERT" << std::endl;
        } break;
        case LogLevel::Critical: {
          std::cout << "CRITICAL" << std::endl;
        } break;
        case LogLevel::Error: {
          std::cout << "ERROR" << std::endl;
        } break;
        case LogLevel::Warning: {
          std::cout << "WARN" << std::endl;
        } break;
        case LogLevel::Notice: {
          std::cout << "NOTICE" << std::endl;
        } break;
        case LogLevel::Info: {
          std::cout << "INFO" << std::endl;
        } break;
        case LogLevel::Debug: {
          std::cout << "DEBUG" << std::endl;
        } break;
        case LogLevel::MAX: {
          std::cout << "Incorrect value" << std::endl;
        } break;
      }
    
      return 0;
    }
    

    Как по мне, в дополнительном объяснении не нуждается.

    Также, залил на github.

    Любезно приглашаю критиков на ревью.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 12

      +1
        0
        Как вариант
          –1
          Я не хотел бы хвастаться, но, похоже, у меня наиболее полная реализация вывода enum'ов.
          1. реализованы не только «обычные» перечисления, но и битовые поля, и смешанные enum/bitfields
          2. реализованы операции +- и битовая арифметика для bitfields (важно для enum class'ов)
          3. поддерживается ручное присвоение числовых значений
          4. можно как объявлять enum, так и адаптировать уже существующий
          5. поддерживается enum внутри класса
          6. поддерживается до 256 значений
          7. есть манипуляторы enum_open, enum_close, bitfield_delim для управления открывающим тэгом, закрывающим тэгом и разделителем


          Недостатки
          1. Зависит от boost.preprocessor


          Синтаксис примерно похожий:

          DECLARE_ENUM
          (
            my_enum,
            (ea)         // 0
            (eb, 10)   // 10
            (ec)         // 11
          );
          
          std::cout << ea << "\n"; // [ea]
          
          // or
          DECLARE_BITFIELD
          (
            my_bitfield,
            (ba, 1)     
            (bb, 2)   
            (bc, 4)   
          );
          
          std::cout << (ba | bc) << "\n"; // [ba, bc]
          
          
        +1

        Хорошо, что я пишу на C++/Qt и таких проблем нет — всё уже реализовано из коробки!

          0

          Както скучновато получилось. А чем это лучше std:map?
          Поскольку значения известны на этапе компиляции, можно было сгенерировать шаблонами компилируемый поиск по префиксному дереву, например, в стиле Александреску. Макросы скорее для чистого Си.

            0
            MAX уберите, это антипаттерн т.к. рушит саму идею энамов — перечисление опций, плюс реализация не задалась:
            std::cout << enum_to_string(LogLevel, LogLevel::MAX) << '\n'; // что-то выведет

            кстати на практике писать придётся как-то так:
            auto x = get_value();
            enum_to_string(decltype(x), x);

            и как то мне не нравится это повторение.
            Хорошие новости что этого всего можно избежать при помощи С++11.
              +3

              Есть такая прекрасная библиотека magic_enum. Работает отлично, не требует никаких дополнительных обвесов вокруг enum-ов.


              https://github.com/Neargye/magic_enum

                0

                Еще бы объяснения того, как она работает. Откуда библиотека берет имена перечислений? Без поддержки от компилятора тут не обойтись. Суда по коду, делается какой-то грязный трюк с __PRETTY_FUNCTION__: https://github.com/Neargye/magic_enum/blob/59aa63ac647b9747443a7e9b688ad3dcfc687fcf/include/magic_enum.hpp#L299-L313


                Автор хоть бы удосужился комментарий к функции написать об идеи решения! А если там баг найдут, а у автора не будет времени на его исправление? Не очень хочется погружаться во все эти дебри не понимая идеи.

                  0
                  Ну если открыть документацию на __PRETTY_FUNCTION__ то всё очевидно:
                  внутри n() __PRETTY_FUNCTION__ развернётся в «return_type n()» и т.к. тип фиксирован то можно использовать константные смещения для вычленения желаемой EnumValue из этой строки.
                  Минус подхода в том, что он не знает как enum объявлен, поэтому например для полного списка значений он перебирает значения в задаваемом диапазоне и пытается сконвертировать их в желаемый enum.
                    0

                    Ну, документация GCC лишь говорит, что она есть и что-то красивое там будет. Другой документации и для clang тоже, что то не находится.


                    Я на само деле уже покопался в исходниках. Идея метода такая:


                    • заводим пару шаблонных функций, вычисляемых во время компиляции:


                      /// E -- тип перечисления
                      template<typename E>
                      constexpr const char* enum_name() { ... }
                      /// E -- тип перечисления
                      /// value -- одно конкретное значение этого типа
                      template<typename E, E value>
                      constexpr const char* enum_value_name() { ... }

                    • Внутри функций используем __PRETTY_FUNCTION__. Благодаря шаблону имя нашего перечисления или значения станет частью имени функции


                    • Вычленяем из имени функции интересующие нас части



                    Вообщем, очень остроумное решение, респект автору. Жалко, что оно немножко нестабильное. Так, не факт, что value в enum_value_name будет именем константы, а не (E)число, как делает clang в режиме -std=c++14 (возможно из-за этого ограничение на -std=c++17).


                    Кроме того, есть еще вопрос, как из имени перечисления получить список список всех его допустимых значений. Если я правильно понял, это делается перебором всех числовых значений в некотором диапазоне (по умолчанию [-128; 128]), трактованием его как элемента перечисления и попыткой взять имя этого элемента. Если имя есть, то это элемент перечисления, а если нет — то просто число

                0

                Надеялся увидеть решение на основе парсера clang

                  +2
                  static int string_to_enum_int(const char* const tokens[], int max,
                                                const char* value) {
                    for (int i = 0; i < max; ++i) {
                      if (string_equals(tokens[i], value)) {
                        return i;
                      }
                    }
                  
                    return max;
                  }


                  Серьёзно? Конвертирование из строки в энум линейной сложности?

                  Only users with full accounts can post comments. Log in, please.