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:

#include 

#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:

#include 

#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:

#include 

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

9 Replies to “enum to string, take 2”

  1. We could call this technique ‘preprocessor polymorphism’. Or a preprocessor Functor. Woohoo! Category theory for the working preprocessor!

  2. 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.

    1. 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.

  3. 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.

  4. 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

Leave a Reply to Howard RosenornCancel reply