Лучшие способы преобразования 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 <iostream>
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()
{
std::cout << EsperToString(Esper::Kvin) << std::endl;
}
Динамическая версия
Дело в том, что наличие нескольких возвратов в функции constexpr
– это C++14. До C++14 вы можете удалить спецификатор constexpr
, чтобы написать динамическую версию этой функции.
#include <iostream>
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()
{
std::cout << EsperToString(Esper::Kvin) << std::endl;
}
До C++11 вы можете удалить спецификатор enum class
и вместо этого использовать простой enum
.
Недостатки
- Наличие нескольких возвратов в функции
constexpr
– это С++14 (для статической версии). - Необходима отдельная функция для каждого перечисления, и очень много кода.
- Является небезопасной относительно исключений.
Использование специальной функции, безопасной относительно исключений
Статическая версия
Иногда вы предпочитаете код, который не выбрасывает исключений. Или, может быть, вы похожи на меня и компилируете с -Werror
. Если это так, вы можете написать функцию, безопасную относительно исключений, без случая default
.
Вам просто нужно следить за предупреждениями, когда добавляете новый элемент в enum
.
#include <iostream>
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()
{
std::cout << EsperToString(Esper::Kvin) << std::endl;
}
Динамическая версия
Опять же, динамическая версия без constexpr
:
#include <iostream>
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()
{
std::cout << EsperToString(Esper::Kvin) << std::endl;
}
До C++11 вы можете удалить спецификатор enum class
и вместо этого использовать простой enum
.
Недостатки
- Наличие нескольких возвратов в функции
constexpr
– это С++ 14 (для статической версии). - Необходима отдельная функция для каждого перечисления, и очень много кода.
- Предупреждения склонны игнорироваться.
Использование макросов
Макросы могут делать многое, чего не может динамический код. Вот две реализации с использованием макросов.
Статическая версия
#include <iostream>
#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<typename T>\
constexpr const char *name##ToString(T value) { return name##Strings[static_cast<int>(value)]; }
ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek);
int main()
{
std::cout << EsperToString(Esper::Kvin) << std::endl;
}
Динамическая версия
Очень похоже на статическую, но если вам это нужно в версии до C++11, вам придется избавиться от спецификатора constexpr
. Кроме того, поскольку это версия до C++11, у вас не может быть класса перечисления, вместо этого вам придется использовать простое перечисление.
#include <iostream>
#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()
{
std::cout << EsperToString(Kvin) << std::endl;
}
Недостатки
- Использует макросы.
- Вам нужно писать другой макрос каждый раз, когда вам нужно преобразуемое перечисление с другим количеством элементов (с другим именем макроса, что расстраивает).
Использование макросов и Boost
Мы можем обойти недостаток «фиксированного количества элементов перечисления» предыдущей версии, используя Boost.
Статическая версия
#include <iostream>
#include <boost/preprocessor.hpp>
#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<typename T>\
constexpr const char *name##ToString(T value) { return name##Strings[static_cast<int>(value)]; }
ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek);
int main()
{
std::cout << EsperToString(Esper::Kvin) << std::endl;
}
Здесь PROCESS_ONE_ELEMENT
«преобразует» элемент в его строковую версию (вызывая BOOST_PP_STRINGIZE
), а BOOST_PP_SEQ_FOR_EACH_I
перебирает каждый элемент __VA_ARGS__
(который представляет собой весь пакет параметров макроса).
Динамическая версия
Опять же, эта версия очень похожа на статическую, но без constexpr
или других спецификаторов C++11.
#include <iostream>
#include <boost/preprocessor.hpp>
#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()
{
std::cout << EsperToString(Kvin) << std::endl;
}
Недостатки
- Использует макросы.
- Использует Boost.
Несмотря на то, что библиотека boost всё еще является сторонней библиотекой, она часто более популярна, чем другие библиотеки (например, малоизвестная библиотека Magic Enum), поэтому (среди прочего) эта версия может быть предпочтительнее первой.
Подведение итогов
В таблице ниже приведено краткое изложение методов, описанных в статье:
Название | Является статическим? | Является обобщенным? | Используются сторонние библиотеки? | Использует макросы? | Безопасно относительно исключений? |
---|---|---|---|---|---|
Magic Enum | Да (C++17) | Да | Да (Magic Enum) | Нет | Нет |
Функция с исключением | Да (C++14) | Нет | Нет | Нет | Нет |
Функция без исключения | Да (C++14) | Нет | Нет | Нет | Да |
Макрос | Да (C++11) | Нет | Нет | Да | Да |
Макросы и Boost | Да (C++11) | Да | Да (Boost) | Да | Да |
Опять же, если вы знаете хороший способ преобразования перечислений в строки, расскажите об этом в комментариях.