Thanks to Sebastian Mestre (who commented on my previous post) I learned something new today 🙂 The X macro makes the enum to string code much… well, perhaps not cleaner, but shorter 😉 It also solves the problem of having to update the code in 2 places: 1st in the enum itself, 2nd in the enum to string map.
Without further ado I present to you the X macro:
And the complete implementation goes something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> #define MY_ENUM \ X(V1) \ X(V2) \ X(V3) #define X(name) name, enum MyEnum { MY_ENUM }; #undef X constexpr const char* MyEnumToString(MyEnum e) noexcept { #define X(name) case(name): return #name; switch(e) { MY_ENUM } #undef X } int main(int argc, char** argv) { std::cout << MyEnumToString(V1) << std::endl; std::cout << MyEnumToString(V2) << std::endl; std::cout << MyEnumToString(V3) << std::endl; return 1; } |
In this version we only have to update the MY_ENUM macro. The rest is taken care of by the preprocessor.
UPDATE
Here’s the same approach that works with strongly typed enums:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> #define MY_ENUM \ X(V1) \ X(V2) \ X(V3) #define X(name) name, #define MY_ENUM_NAME MyEnum enum class MY_ENUM_NAME : char { MY_ENUM }; #undef X constexpr const char* MyEnumToString(MyEnum e) noexcept { #define X(name) case(MY_ENUM_NAME::name): return #name; switch(e) { MY_ENUM } #undef X } int main(int argc, char** argv) { std::cout << MyEnumToString(MyEnum::V1) << std::endl; std::cout << MyEnumToString(MyEnum::V2) << std::endl; std::cout << MyEnumToString(MyEnum::V3) << std::endl; return 1; } |
UPDATE 2
Here’s the same approach that works with strongly typed enums, custom enum values, and enum values outside of the defined ones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <iostream> #define MY_ENUM \ X(V1, -1) \ X(V2, -3) \ X(V3, -5) #define X(name, value) name = value, #define MY_ENUM_NAME MyEnum #define MY_ENUM_TYPE int enum class MY_ENUM_NAME : MY_ENUM_TYPE { MY_ENUM }; #undef X constexpr auto MyEnumToString(MY_ENUM_NAME e) noexcept { #define X(name, value) case(MY_ENUM_NAME::name): return #name; switch(e) { MY_ENUM } #undef X return "UNKNOWN"; } int main(int argc, char** argv) { std::cout << "value = " << (MY_ENUM_TYPE)MyEnum::V1 << ", name = " << MyEnumToString(MY_ENUM_NAME::V1) << std::endl; std::cout << "value = " << (MY_ENUM_TYPE)MyEnum::V2 << ", name = " << MyEnumToString(MY_ENUM_NAME::V2) << std::endl; std::cout << "value = " << (MY_ENUM_TYPE)MyEnum::V3 << ", name = " << MyEnumToString(MY_ENUM_NAME::V3) << std::endl; MY_ENUM_NAME unknown{-42}; std::cout << "value = " << (MY_ENUM_TYPE)unknown << ", name = " << MyEnumToString(unknown) << std::endl; return 1; } |
We could call this technique ‘preprocessor polymorphism’. Or a preprocessor Functor. Woohoo! Category theory for the working preprocessor!
I would follow the C++ Core Guideline “ES.30: Don’t use macros for program text manipulation”.
Just implement the enum_to_string function with a switch and explicit cases. Plain and understandable for everybody. Modern compiler will warn when there is a missing case switch when using a enum class.
Code with macro’s are difficult to debug, produce difficult compiler warning/error messages.
IMHO: plain switch has also more flexibility (load string from resource based on user locale, for example) or different string for a certain value and maintenance gain is small.
Note 1: MyEnumToString can be made constexpr + noexcept.
Note 2: of course plain switch can be “upgraded” when c++20 makes reflection possible.
Victor,
Thank you for your suggestion in “Note 1”; I updated the post.
sorry for the stupid question, but are you not allowed to use C++11?
otherwise you can simply use Better enums (https://github.com/aantron/better-enums)
One problem with that header is that it’s too much apparently impenetrable code to relate to when something goes wrong. And Visual C++ and g++ differ in their preprocessor semantics. Things can go wrong, just with some option or new version, whatever.
Instead of a switch inside the function, expand it to a sequence of stringified values inside an array. Then your enum is a direct map to an array entry containing the string. Much faster than the switch. And gag wash to people who say not to use the preprocessor. It’s a tool. Use it. If people can’t figure out the code let them use php.
This technique is entirely valid and supported in every c++ preprocessor I know of. You can actually use this to do much more complex things. Ive used it to declare on a single line a lot of information and then from that build out multiple enums, arrays and structures. It makes it much easier to isolate updates to common data to a single place rather than having it scattered throughout your code.
One change I would make to the code would be the below. It provides feed back for an unknown value and will also remove the warning: control reaches end of non-void function.
constexpr const char* MyEnumToString(MyEnum e) noexcept
{
#define X(name) case(MY_ENUM_NAME::name): return #name;
switch(e)
{
MY_ENUM
}
#undef X
return “UNKNOWN”;
}
This is a nice option except it doesn’t seem to allow a starting value. For example I have an enum list of errors but they are all negative values. So for this I need to start from the lowest negative value and let it increment up. Is there a way to do that?
enum class MY_ENUM_NAME : int8_t
{
CONNECTION_LOST_TO_HOST = -32,
SOMETHING_BROKE_HERE,
…
SUCCESS, // This will be 0
Howard,
I updated the blog post with your suggestions and added the ability to define custom enum values. The blog post as well as my GitHub contains the new code: https://github.com/mvorbrodt/blog/blob/master/src/enum2.cpp
HTH
Martin
I described another possible solution to this problem here: https://sheep.horse/2018/5/converting_enum_classes_to_strings_and_back_in_c%2B%2B.html
It works using strongly-typed templated helpers instead of preprocessor macros and is slightly more flexible in what it can do. It is possible that the switch statement you build would be slightly faster at runtime, but I am not sure.