Best ways to convert an enum to a string

Author: Chloé Lourseyre

One of the oldest problem C++ developers ever encountered is how to print the value of an enum type.

All right, I may be a little overly dramatic here, but this is an issue many C++ developers encountered, even the most casual ones.

The thing is, there isn’t one true answer to that issue. It depends on many things such as your constraints, your needs, and, as always, the C++ version of your compiler.

This article is a small list of ways to add reflection to enums.

NB: If you know of a way that isn’t listed here and has its own benefits, feel free to share in the comments.

Magic Enum library

Magic Enum is a header-only library that gives static reflection to enums.

You can convert from and to strings and you can iterate over the enum values. It adds the “enum_cast” feature.

Find it here: GitHub – Neargye/magic_enum: Static reflection for enums (to string, from string, iteration) for modern C++, work with any enum type without any macro or boilerplate code

Drawbacks

  • It’s a third-party library.
  • Only works in C++17.
  • You need specific versions of your compiler for it to work (Clang >= 5, MSVC >= 15.3 and GCC >= 9).
  • You have a few other constraints related to the implementation of the library. Check the Limitations page of the documentation (magic_enum/limitations.md at master · Neargye/magic_enum · GitHub)

Using a dedicated function with an exception

Static version

constexpr is a magnificent tool that allows us to statically define stuff. When used as the return value of a function, it allows us to evaluate the return value of the function at compile time.

In this version, I added an exception in the default, so if it happens so that an item is added, the exception will be raised.

#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;
}

Dynamic version

The thing is, having several returns in a constexpr function is C++14. Prior to C++14, you can remove the constexpr specifier to write a dynamic version of this function

#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;
}

Prior to C++11, you can remove the enum class specifier and use a plain enum instead.

Drawbacks

  • Having several returns in a constexpr function is C++14 (for the static version).
  • Specific to each enum and very verbose.
  • Is exception-unsafe.

Using a dedicated exception-safe function

Static version

Sometimes you prefer a code that doesn’t throw, no matter what. Or maybe you are like me and compile with -Werror. If so, you can write an exception-safe function without the default case.

You just have to watch out for warnings when you need to add an item.

#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;
}

Dynamic version

Again, a dynamic version without 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;
}

Prior to C++11, you can remove the enum class specifier and use a plain enum instead.

Drawbacks

  • Having several returns in a constexpr function is C++14 (for the static version).
  • Specific to each enum and very verbose.
  • The warnings are prone to be ignored.

Using macros

Macros can do many things that dynamic code can’t do. Here are two implementations using macros.

Static version

#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;
}

Dynamic version

Very similar to the static one, but if you need this in a version prior to C++11, you’ll have to get rid of the constexpr specifier. Also, since it’s a pre-C++11 version, you can’t have enum class, you’ll have to go with a plain enum instead.

#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;
}

Drawbacks

  • Uses macros (I could — has and will — write an article about why macros are bad practice in C++, but won’t do it here. For now, just keep in mind that if you don’t know why macros can be bad, then you shouldn’t use them)
  • You need to write a different macro each time you need a reflective enum with a different number of items (with a different macro name, which is upsetting).

Using macros and boost

We can work around the “fixed number of enum items” drawback of the previous version by using Boost.

Static version

#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;
}

Here, the PROCESS_ONE_ELEMENT “converts” the item to its stringized version (calling BOOST_PP_STRINGIZE), and the BOOST_PP_SEQ_FOR_EACH_I iterates over every item of __VA_ARGS__ (which is the whole macro’s parameter pack).

Dynamic version

Again, it’s a very similar version of the static one, but without the constexpr or other C++11 specifiers.

#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;
}

Drawbacks

  • Uses macros.
  • Uses Boost.

NB: While still being a third-party library, the boost library is often more accepted than other libraries (such as the little-known Magic Enum library), that’s (inter alia) why this version may be preferred to the first one.

Wrapping up

Here is a little summary of the methods described here:

NameIs static?Is generic?3rd party librariesUses macros?Is exception-safe?
Magic EnumYes (C++17)YesYes (Magic Enum)NoNo
Function w/ exceptionYes (C++14)NoNoNoNo
Function w/o exception Yes (C++14)NoNoNoYes
MacroYes (C++11)NoNoYesYes
Macro & BoostYes (C++11)YesYes (Boost)YesYes

Again, if you know a good way to convert enums to string, please say so in the comments.

Thanks for reading and see you next week!

Author: Chloé Lourseyre

10 thoughts on “Best ways to convert an enum to a string”

  1. I believe the best way is smart usage of macro and preprocessor.
    Let’s imagine I’m going to create an enum of colors.

    Instead of classic
    enum class Colors { Red, Blue, … };

    I’ll do
    enum class Colors
    {
    #define COLOR_DEF(NAME, VALUE) NAME = VALUE,
    #include « Colors.hh »
    #undef COLOR_DEF
    };

    where Colors.hh is a file with following content
    COLOR_DEF(Red, 0)
    COLOR_DEF(Blue, 1)

    COLOR_DEF(Count, N)

    In order to convert Colors to text I’ll write a function
    const char* foo(Colors color)
    {
    #define COLOR_DEF(NAME, VALUE) return #NAME;
    #include « Colors.hh »
    #undef COLOR_DEF
    }

    This is very convenient once you’ll get used to it – you don’t lookup through the codebase is order to add one new value to the enum. You simply change one file and that’s it.

  2. Pingback: Cpp enum to char

Leave a Reply