Cpp enum with string

Лучшие способы преобразования enum в строку

Одна из старейших проблем, с которыми когда-либо сталкивались разработчики C++, заключается в том, как напечатать значение перечислимого типа.

Хорошо, может быть, это немного слишком драматично, но с этой проблемой сталкивались многие разработчики C++, даже самые начинающие.

Дело в том, что на этот вопрос нет однозначного ответа. Это зависит от многих вещей, таких как ваши ограничения, ваши потребности и, как всегда, версия C++ вашего компилятора.

Данная статья представляет собой небольшой список способов добавить текстовое пояснение в перечисления.

Если вы знаете способ, который не указан здесь и имеет свои преимущества, поделитесь им в комментариях.

Библиотека Magic Enum

Magic Enum – это библиотека header-only (только из заголовков), которая обеспечивает статическое преобразование перечислений.

Вы можете конвертировать из строк и в строки, а также выполнять итерации по значениям перечисления. Она добавляет функцию enum_cast .

Её можно найти на GitHub – Neargye/magic_enum: статическая работа с перечислениями (преобразование в строку, из строки, итерация) для современного C++, работа с любым типом перечисления без макросов или шаблонного кода.

  • Это сторонняя библиотека.
  • Работает, начиная только с C++17.
  • Для её работы вам нужны определенные версии вашего компилятора (Clang >= 5, MSVC >= 15.3 и GCC >= 9).
  • У вас есть несколько других ограничений, связанных с реализацией библиотеки (проверьте страницу «Ограничения» в документации).

Использование специальной функции с исключением

Статическая версия

constexpr – великолепный инструмент, который позволяет нам статически определять вещи. При использовании в качестве возвращаемого значения функции он позволяет нам вычислить это возвращаемое значение функции во время компиляции.

В этой версии я добавил исключение в default , поэтому, если случится так, что элемент будет добавлен в enum , но не обработан в данной функции, то будет выкинуто исключение.

#include enum class Esper < Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek >; constexpr const char* EsperToString(Esper e) throw() < switch (e) < case Esper::Unu: return "Unu"; case Esper::Du: return "Du"; case Esper::Tri: return "Tri"; case Esper::Kvar: return "Kvar"; case Esper::Kvin: return "Kvin"; case Esper::Ses: return "Ses"; case Esper::Sep: return "Sep"; case Esper::Ok: return "Ok"; case Esper::Naux: return "Naux"; case Esper::Dek: return "Dek"; default: throw std::invalid_argument("Unimplemented item"); >> int main()

Динамическая версия

Дело в том, что наличие нескольких возвратов в функции constexpr – это C++14. До C++14 вы можете удалить спецификатор constexpr , чтобы написать динамическую версию этой функции.

#include enum class Esper < Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek >; const char* EsperToString(Esper e) throw() < switch (e) < case Esper::Unu: return "Unu"; case Esper::Du: return "Du"; case Esper::Tri: return "Tri"; case Esper::Kvar: return "Kvar"; case Esper::Kvin: return "Kvin"; case Esper::Ses: return "Ses"; case Esper::Sep: return "Sep"; case Esper::Ok: return "Ok"; case Esper::Naux: return "Naux"; case Esper::Dek: return "Dek"; default: throw std::invalid_argument("Unimplemented item"); >> int main()

До C++11 вы можете удалить спецификатор enum class и вместо этого использовать простой enum .

  • Наличие нескольких возвратов в функции constexpr – это С++14 (для статической версии).
  • Необходима отдельная функция для каждого перечисления, и очень много кода.
  • Является небезопасной относительно исключений.
Читайте также:  Java stream api терминальные операторы

Использование специальной функции, безопасной относительно исключений

Статическая версия

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

Вам просто нужно следить за предупреждениями, когда добавляете новый элемент в enum .

#include enum class Esper < Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek >; constexpr const char* EsperToString(Esper e) noexcept < switch (e) < case Esper::Unu: return "Unu"; case Esper::Du: return "Du"; case Esper::Tri: return "Tri"; case Esper::Kvar: return "Kvar"; case Esper::Kvin: return "Kvin"; case Esper::Ses: return "Ses"; case Esper::Sep: return "Sep"; case Esper::Ok: return "Ok"; case Esper::Naux: return "Naux"; case Esper::Dek: return "Dek"; >> int main()

Динамическая версия

Опять же, динамическая версия без constexpr :

#include enum class Esper < Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek >; const char* EsperToString(Esper e) noexcept < switch (e) < case Esper::Unu: return "Unu"; case Esper::Du: return "Du"; case Esper::Tri: return "Tri"; case Esper::Kvar: return "Kvar"; case Esper::Kvin: return "Kvin"; case Esper::Ses: return "Ses"; case Esper::Sep: return "Sep"; case Esper::Ok: return "Ok"; case Esper::Naux: return "Naux"; case Esper::Dek: return "Dek"; >> int main()

До C++11 вы можете удалить спецификатор enum class и вместо этого использовать простой enum .

  • Наличие нескольких возвратов в функции constexpr – это С++ 14 (для статической версии).
  • Необходима отдельная функция для каждого перечисления, и очень много кода.
  • Предупреждения склонны игнорироваться.

Использование макросов

Макросы могут делать многое, чего не может динамический код. Вот две реализации с использованием макросов.

Статическая версия

#include #define ENUM_MACRO(name, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)\ enum class name < v1, v2, v3, v4, v5, v6, v7, v8, v9, v10 >;\ const char *name##Strings[] = < #v1, #v2, #v3, #v4, #v5, #v6, #v7, #v8, #v9, #v10>;\ template\ constexpr const char *name##ToString(T value) < return name##Strings[static_cast(value)]; > ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek); int main()

Динамическая версия

Очень похоже на статическую, но если вам это нужно в версии до C++11, вам придется избавиться от спецификатора constexpr . Кроме того, поскольку это версия до C++11, у вас не может быть класса перечисления, вместо этого вам придется использовать простое перечисление.

#include #define ENUM_MACRO(name, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)\ enum name < v1, v2, v3, v4, v5, v6, v7, v8, v9, v10 >;\ const char *name##Strings[] = < #v1, #v2, #v3, #v4, #v5, #v6, #v7, #v8, #v9, #v10 >;\ const char *name##ToString(int value) < return name##Strings[value]; >ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek); int main()
  • Использует макросы.
  • Вам нужно писать другой макрос каждый раз, когда вам нужно преобразуемое перечисление с другим количеством элементов (с другим именем макроса, что расстраивает).

Использование макросов и Boost

Мы можем обойти недостаток «фиксированного количества элементов перечисления» предыдущей версии, используя Boost.

Статическая версия

#include #include #define PROCESS_ONE_ELEMENT(r, unused, idx, elem) \ BOOST_PP_COMMA_IF(idx) BOOST_PP_STRINGIZE(elem) #define ENUM_MACRO(name, . )\ enum class name < __VA_ARGS__ >;\ const char *name##Strings[] = < BOOST_PP_SEQ_FOR_EACH_I(PROCESS_ONE_ELEMENT, %%, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) >;\ template\ constexpr const char *name##ToString(T value) < return name##Strings[static_cast(value)]; > ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek); int main()

Здесь PROCESS_ONE_ELEMENT «преобразует» элемент в его строковую версию (вызывая BOOST_PP_STRINGIZE ), а BOOST_PP_SEQ_FOR_EACH_I перебирает каждый элемент __VA_ARGS__ (который представляет собой весь пакет параметров макроса).

Читайте также:  Ropeway am s1 html

Динамическая версия

Опять же, эта версия очень похожа на статическую, но без constexpr или других спецификаторов C++11.

#include #include #define PROCESS_ONE_ELEMENT(r, unused, idx, elem) \ BOOST_PP_COMMA_IF(idx) BOOST_PP_STRINGIZE(elem) #define ENUM_MACRO(name, . )\ enum name < __VA_ARGS__ >;\ const char *name##Strings[] = < BOOST_PP_SEQ_FOR_EACH_I(PROCESS_ONE_ELEMENT, %%, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) >;\ const char *name##ToString(int value) < return name##Strings[value]; >ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek); int main()

Несмотря на то, что библиотека boost всё еще является сторонней библиотекой, она часто более популярна, чем другие библиотеки (например, малоизвестная библиотека Magic Enum), поэтому (среди прочего) эта версия может быть предпочтительнее первой.

Подведение итогов

В таблице ниже приведено краткое изложение методов, описанных в статье:

Название Является статическим? Является обобщенным? Используются сторонние библиотеки? Использует макросы? Безопасно относительно исключений?
Magic Enum Да (C++17) Да Да (Magic Enum) Нет Нет
Функция с исключением Да (C++14) Нет Нет Нет Нет
Функция без исключения Да (C++14) Нет Нет Нет Да
Макрос Да (C++11) Нет Нет Да Да
Макросы и Boost Да (C++11) Да Да (Boost) Да Да

Опять же, если вы знаете хороший способ преобразования перечислений в строки, расскажите об этом в комментариях.

Теги

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.

Источник

Удобное преобразование перечислений (enum) в строковые в С++

У перечислений есть множество способов применения в разработке. Например, при создании игр они используются для программирования состояний персонажа или возможных направлений движения:

enum State ; enum Direction ; 

Гораздо удобнее, когда во время отладки в консоль выводится сообщение типа “ State: Fidget ” вместо “ State: 1 ”. Также частенько бывает нужно сериализировать перечисления в JSON, YAML или иной формат, причём в виде строковых значений. Помимо того, что строковые воспринимать легче, чем числа, их применение в формате сериализации повышает устойчивость к изменениям численных значений констант перечислений. В идеале, «Fidget» должен ссылаться на Fidget , даже если объявлена новая константа, а Fidget имеет значение, отличное от 1.

К сожалению, в С++ нет возможности легко конвертировать значения перечислений в строковые и обратно. Поэтому разработчики вынуждены прибегать к разным ухищрениям, которые требуют определённой поддержки: жёстко закодированным преобразованиям или к использованию неприглядного ограничительного синтаксиса, наподобие Х-макросов. Кто-то дополнительно использует средства сборки для автоматического преобразования. Естественно, это только усложняет процесс разработки. Ведь перечисления имеют свой собственный синтаксис и хранятся в собственных входных файлах, что не облегчает работу средств сборки в Makefile или файлах проекта.

Однако средствами С++ можно гораздо проще решить задачу преобразования перечислений в строковые.

Есть возможность избежать всех упомянутых трудностей и генерировать перечисления с полной рефлексией на чистом С++. Объявление выглядит так:

BETTER_ENUM(State, int, Idle, Fidget, Walk, Scan, Attack) BETTER_ENUM(Direction, int, North, South, East, West) 
State state = State::Fidget; state._to_string(); // "Fidget" std::cout 

Это делается с помощью нескольких ухищрений, связанных с препроцессором и шаблоном. О них мы немного поговорим в конце статьи.

Помимо преобразования в строковые и обратно, а также поточного ввода/вывода, мы можем ещё и перебирать сгенерированные перечисления:

for (Direction direction : Direction._values()) character.try_moving_in_direction(direction); 

Можно сгенерировать перечисления с разреженными диапазонами, а затем подсчитать:

BETTER_ENUM(Flags, char, Allocated = 1, InUse = 2, Visited = 4, Unreachable = 8) Flags::_size(); // 4 

Если вы работаете в С++ 11, то можете даже сгенерировать код на основе перечислений, потому что все преобразования и циклы могут выполняться в ходе компиляции с помощью функций constexpr . Можно, к примеру, написать такую функцию constexpr , которая будет вычислять максимальное значение перечисления и делать его доступным во время компиляции. Даже если значения констант выбираются произвольно и не объявляются в порядке возрастания.

Читайте также:  Php как принять куку

Вы можете скачать с Github пример реализации макроса, упакованного в библиотеку под названием Better Enums (Улучшенные перечисления). Она распространяется под лицензией BSD, так что с ней можно делать что угодно. В данной реализации имеется один заголовочный файл, так что использовать её очень просто, достаточно добавить enum.h в папку проекта. Попробуйте, возможно, это поможет вам в решении ваших задач.

Как это работает

Для осуществления преобразований между строковыми и значениями перечислений необходимо сгенерировать соответствующий маппинг. Better Enums делает это с помощью создания двух массивов в ходе компиляции. Например, если у вас есть такое объявление:

BETTER_ENUM(Direction, int, North = 1, South = 2, East = 4, West = 8) 

то макрос переделает его в нечто подобное:

struct Direction < enum _Enum : int ; static const int _values[] = ; static const char * const _names[] = ; int _value; // . Функции, использующие вышеприведённое объявление. >; 

А затем перейдет к преобразованию: найдет индекс значения или строковой в _values или _names и вернет его соответствующее значение или строковую в другой массив.

Массив значений

_values генерируется путём обращения к константам внутреннего перечисления _Enum . Эта часть макроса выглядит так:

Это почти правильное объявление массива. Проблема заключается в дополнительных инициализаторах вроде «= 1». Для работы с ними Better Enums определяет вспомогательный тип, предназначенный для оператора присваивания, но игнорирует само присваиваемое значение:

template struct _eat < T _value; template _eat& operator =(Any value) < return *this; >// Игнорирует аргумент. explicit _eat(T value) : _value(value) < >// Преобразует из T. operator T() const < return _value; >// Преобразует в T. > 

Теперь можно включить инициализатор «= 1» в выражение присваивания, не имеющее значения:

 static const int _values[] = <(_eat)North = 1, (_eat)South = 2, (_eat)East = 4, (_eat)West = 8>; 

Массив строковых

Для создания этого массива Better Enums использует ( # ) — препроцессорный оператор перевода в строковое (stringization). Он конвертирует __VA_ARGS__ в нечто подобное:

 static const char * const _names[] = ; 

Теперь мы почти преобразовали имена констант в строковые. Осталось избавиться от инициализаторов. Однако Better Enums этого не делает. Просто при сравнении строковых в массиве _names он воспринимает символы пробелов и равенства как дополнительные границы строк. Так что при поиске “ North = 1 ” Better Enums найдёт только “ North ”.

Можно ли обойтись без макроса?

Вряд ли. Дело в том, что в С++ оператор (#) — единственный способ преобразования токена исходного кода в строковое. Так что в любой библиотеке, автоматически преобразующей перечисления с рефлексией, приходится использовать как минимум один высокоуровневый макрос.

Прочие соображения

Конечно, полностью рассматривать реализацию макроса было бы гораздо скучнее и сложнее, чем это сделано в данной статье. В основном, сложности возникают из-за поддержки работающих с массивами static функций constexpr , из-за особенностей разных компиляторов. Также определённые затруднения могут быть связаны с разложением как можно большей части макроса на шаблоны ради ускорения компиляции (шаблоны не нужно репарсить в ходе создания, а расширения-макросы — нужно).

Источник

Оцените статью