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.
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:
Name | Is static? | Is generic? | 3rd party libraries | Uses macros? | Is exception-safe? |
Magic Enum | Yes (C++17) | Yes | Yes (Magic Enum) | No | No |
Function w/ exception | Yes (C++14) | No | No | No | No |
Function w/o exception | Yes (C++14) | No | No | No | Yes |
Macro | Yes (C++11) | No | No | Yes | Yes |
Macro & Boost | Yes (C++11) | Yes | Yes (Boost) | Yes | Yes |
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
Why do you consider Magic Enum exception-unsafe?
http://aantron.github.io/better-enums/ is awesome too!
If you’re going to use macros, this is a much better technique:
https://digitalmars.com/articles/b51.html
It’s useful for more than just converting enums to strings and really deserves to be more widely known.
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.