3
\$\begingroup\$

I needed to find a way to convert a bunch of enums to string for displaying in C++. The two main ways I found when searching was using dark magic macros or voodoo magic template metaprogramming.

As I don't know much about any of them I thought building something myself would be a good opportunity to learn new things. Here are the features I wanted:

  • enum to string only (no string to enum)
  • can be added on top of legacy code (no change in enum declaration)
  • mapping generated at compile time
  • can use __PRETTY_FUNCTION__ (clang/gcc) or __FUNCSIG__ (msvc), but with a fallback displaying something somewhat useful on compilers without them
  • works on enum declared inside a class
  • works on enum with duplicate values (ok to return identical strings)
  • use as little macro as possible, and limited to "simple" ones (e.g. defining a function is ok, defining 64 int-postfixed macros chains is not)
  • can be used for small contiguous enums as well as ones with large arbitrary gaps between values
  • fully automatic mapping, or at least don't crash at runtime if enum was updated with new value and the string conversion call was not
  • be as simple as possible so future me will still be able to understand what's is going on

Here is the result! I'm strill trying to simplify the declaration process to remove the macro and/or allow calling it right below the enum declaration in a header file but I haven't figured it out yet.

Live example

Usage example

#include <iostream>

enum class Test : char
{
    neg = -8,
    a = 0,
    more_a = a,
    b = 3,
    c
};
// Looks like someone forgot to update after adding Test::c, no problem
DEFINE_ENUM_STRINGIFYER_RANGE(Test, Test::neg, Test::b);

enum class Test2 : unsigned short
{
    a = 1 << 0,
    b = 1 << 1,
    c = 1 << 2,
    d = 1 << 3,
    e = 1 << 4,
    f = 1 << 5,
    g = 1 << 6,
    h = 1 << 7,
    i = 1 << 8,
    j = 1 << 9,
};
DEFINE_ENUM_STRINGIFYER_LIST(Test2,
    Test2::a,
    Test2::b,
    Test2::c,
    Test2::d,
    Test2::e,
    // forgot f, everything is still fine
    Test2::g,
    Test2::h,
    Test2::i,
    Test2::j);

int main()
{
    std::cout 
        << Test::neg << '\n'     // Test::neg
        << Test::a << '\n'       // Test::a
        << Test::more_a << '\n'  // Test::a
        << Test::b << '\n'       // Test::b
        << Test::c << '\n'       // (Test)4
        << '\n'
        << Test2::a << '\n'      // Test2::a
        << Test2::b << '\n'      // Test2::b
        << Test2::c << '\n'      // Test2::c
        << Test2::d << '\n'      // Test2::d
        << Test2::e << '\n'      // Test2::e
        << Test2::f << '\n'      // (Test2)32
        << Test2::g << '\n'      // Test2::g
        << Test2::h << '\n'      // Test2::h
        << Test2::i << '\n'      // Test2::i
        << Test2::j << '\n';     // Test2::j
}

enum_stringifyer.hpp

#pragma once

#ifndef ENUM_STRINGIFYER_HPP_
#define ENUM_STRINGIFYER_HPP_

#include <type_traits>
#include <string_view>
#include <array>
#include <utility>
#include <algorithm>

namespace enum_stringifyer
{
    template <auto V>
    constexpr auto enum_value_name() noexcept
    {
        static_assert(std::is_enum_v<decltype(V)>, "enum_value_name requires enum value");

        // Extract value from macros, with a -1 on the size cause we don't need the last \0 in a string_view
#if defined(__clang__)// "auto enum_value_name() [V = XXXXXX::XXXXX::XXXXX]"
        constexpr std::string_view header = "auto enum_stringifyer::enum_value_name() [V = ";
        constexpr std::string_view footer = "]";
        return std::string_view{ __PRETTY_FUNCTION__ + header.size(), sizeof(__PRETTY_FUNCTION__) - 1 - header.size() - footer.size() };
#elif defined(__GNUC__)// "constexpr auto enum_value_name() [with auto V = XXXXXX::XXXXX::XXXXX]"
        constexpr std::string_view header = "constexpr auto enum_stringifyer::enum_value_name() [with auto V = ";
        constexpr std::string_view footer = "]";
        return std::string_view{ __PRETTY_FUNCTION__ + header.size(), sizeof(__PRETTY_FUNCTION__) - 1 - header.size() - footer.size() };
#elif defined(_MSC_VER) // "auto __cdecl enum_value_name<XXXXXX::XXXXX::XXXXX>(void) noexcept"
        constexpr std::string_view header = "auto __cdecl enum_stringifyer::enum_value_name<";
        constexpr std::string_view footer = ">(void) noexcept";
        return std::string_view{ __FUNCSIG__ + header.size(), sizeof(__FUNCSIG__) - 1 - header.size() - footer.size() };
#else
        static_assert(false, "enum_value_name requires either __PRETTY_FUNCTION__ or __FUNCSIG__ to be available");
#endif
    }

    template <class F, class T, T ... Is>
    constexpr void constexpr_for(F&& f, std::integer_sequence<T, Is...> const&)
    {
        ((f(std::integral_constant<T, Is>())), ...);
    }

    template<class Enum, Enum m, Enum M>
    struct EnumMapperRange
    {
        static_assert(static_cast<std::underlying_type_t<Enum>>(M) >= static_cast<std::underlying_type_t<Enum>>(m), "M must be >= m");

        constexpr EnumMapperRange() : mapping(), start(static_cast<std::underlying_type_t<Enum>>(m)), end(static_cast<std::underlying_type_t<Enum>>(M))
        {
            // Same as a for loop, but with constexpr friendly i
            //for (auto i = 0; i < end - start + 1; ++i)
            //{
            //    mapping[i] = enum_value_name<static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(m) + i)>();
            //}
            constexpr_for([this](auto i)
                {
                    mapping[i] = enum_value_name<static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(m) + i)>();
                }, std::make_integer_sequence<std::underlying_type_t<Enum>, static_cast<std::underlying_type_t<Enum>>(M) - static_cast<std::underlying_type_t<Enum>>(m) + 1>{});
        }

        std::array<std::string_view, static_cast<std::underlying_type_t<Enum>>(M) - static_cast<std::underlying_type_t<Enum>>(m) + 1> mapping;
        const std::underlying_type_t<Enum> start;
        const std::underlying_type_t<Enum> end;
    };

    template<class Enum, Enum... pack>
    constexpr std::array<std::pair<Enum, std::string_view>, sizeof...(pack)> GetNamedEnum()
    {
        return { std::make_pair(pack, enum_value_name<pack>())... };
    }
}

#define DECLARE_ENUM_STRINGIFYER(Enum) std::ostream& operator <<(std::ostream& os, const Enum v)

#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER)
#define DEFINE_ENUM_STRINGIFYER_RANGE(Enum, min_value, max_value)                                   \
std::ostream& operator <<(std::ostream& os, const Enum v)                                           \
{                                                                                                   \
    static_assert(std::is_same_v<decltype(min_value), Enum>, "min_value must be "#Enum);            \
    static_assert(std::is_same_v<decltype(max_value), Enum>, "max_value must be "#Enum);            \
    static constexpr auto mapper = enum_stringifyer::EnumMapperRange<Enum, min_value, max_value>(); \
    if (static_cast<std::underlying_type_t<Enum>>(v) < mapper.start ||                              \
        static_cast<std::underlying_type_t<Enum>>(v) > mapper.end)                                  \
    {                                                                                               \
        return os << '(' << #Enum << ')' << static_cast<int>(v);                                    \
    }                                                                                               \
    return os << mapper.mapping[static_cast<std::underlying_type_t<Enum>>(v) - mapper.start];       \
} static_assert(true, "") /* To require a ; after macro call */
#else // min_value and max_value are not necessary, but keep them to get the same interface
#define DEFINE_ENUM_STRINGIFYER_RANGE(Enum, min_value, max_value)    \
std::ostream& operator <<(std::ostream& os, const Enum v)            \
{                                                                    \
    return os << '(' << #Enum << ')' << static_cast<int>(v);         \
} static_assert(true, "") /* To require a ; after macro call */
#endif

#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER)
#define DEFINE_ENUM_STRINGIFYER_LIST(Enum, ...)                                         \
std::ostream& operator <<(std::ostream& os, const Enum v)                               \
{                                                                                       \
    static constexpr auto mapper = enum_stringifyer::GetNamedEnum<Enum, __VA_ARGS__>(); \
    const auto it = std::find_if(mapper.begin(), mapper.end(),                          \
        [v](const std::pair<Enum, std::string_view>& p) { return p.first == v; });      \
    if (it == mapper.end())                                                             \
    {                                                                                   \
        return os << '(' << #Enum << ')' << static_cast<int>(v);                        \
    }                                                                                   \
    return os << it->second;                                                            \
} static_assert(true, "") /* To require a ; after macro call */
#else // value list is not necessary, but keep it to get the same interface
#define DEFINE_ENUM_STRINGIFYER_LIST(Enum, ...)   \
std::ostream& operator <<(std::ostream& os, const Enum v)            \
{                                                                    \
    return os << '(' << #Enum << ')' << static_cast<int>(v);         \
} static_assert(true, "") /* To require a ; after macro call */
#endif

#endif
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Given the requirement that this code needs to work with already declared enums which's definitions you cannot change, there really is no way other than __PRETTY_FUNCTION__ or using an experimental implementation of static reflections, like reflexpr.

Your implementation of enum_value_name() makes sense, but everything past that could use a massive re-design. Let's focus on various minor issues first:

Repetition of static_cast<std::underlying_type_t<E>>

C++23 has added std::to_underlying() to eliminate this pattern, and you can easily implement it yourself:

template <typename E>
constexpr std::underlying_type_t<E> to_underlying(E e) noexcept
{
    return static_cast<std::underlying_type_t<E>>(e);
}

Repetition in enum_value_name(), misuse of static_assert

The way you obtain the enum name between header and footer is self-documenting, so the comments are redundant. Also, you do the same thing for every compiler, but repeat your math for finding the right substring. Also, static_assert(false) will always trigger (until C++23, in some situations), so basically you're failing to compile with something other than GCC, clang, or MSVC.

Here is how you can solve these problems:

#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER)
inline constexpr bool has_pretty_name = true;
#else
inline constexpr bool has_pretty_name = false;
#endif

template <auto V>
constexpr auto enum_value_name() noexcept
{
    static_assert(std::is_enum_v<decltype(V)>, "enum_value_name requires enum value");
    // V == V makes the expression dependent on V.
    // The assertion only evaluates if the function template is instantiated.
    static_assert(V == V && has_pretty_name, "enum_value_name requires either __PRETTY_FUNCTION__ or __FUNCSIG__ to be available");

#if defined(__clang__)
    constexpr std::string_view header = "auto enum_stringifyer::enum_value_name() [V = ";
    constexpr std::string_view footer = "]";
    constexpr std::string_view name = __PRETTY_FUNCTION__;
#elif defined(__GNUC__)
    constexpr std::string_view header = "constexpr auto enum_stringifyer::enum_value_name() [with auto V = ";
    constexpr std::string_view footer = "]";
    constexpr std::string_view name = __PRETTY_FUNCTION__;
#elif defined(_MSC_VER)
    constexpr std::string_view header = "auto __cdecl enum_stringifyer::enum_value_name<";
    constexpr std::string_view footer = ">(void) noexcept";
    constexpr std::string_view name = __FUNCSIG__;
#endif
    // extract everything between header and footer
    return name.substr(header.size(), name.length() - header.length() - footer.length());
}

Notice that no matter whether we use __PRETTY_FUNCTION__ or __FUNCSIG__, we can always capture this string literal in a std::string_view, which simplifies this code a bit.

Multiple Issues with EnumMapperRange

  1. The beginning and end of the range are template parameters m and M. No one forces you to use single-character identifiers, let alone same-single-character identifiers with different case. The convention in the C++ standard is for all template parameters to be PascalCase, with sane names.
  2. EnumMapperRange has non-static data members, but all of them are compile-time properties. Making them static inline constexpr would make more sense.
  3. Your code is needlessly generic. You have made a constexpr_for that works with any function and any integer sequence, but you only need it in a single place, doing exactly one thing, with an integer sequence of std::size_t.

Let's fix those problems:

template<class Enum, Enum Begin, Enum End>
struct EnumMapperRange
{
    using underlying_type = std::underlying_type_t<Enum>;

    static inline constexpr underlying_type begin = to_underlying(Begin);
    static inline constexpr underlying_type end = to_underlying(End);
    static_assert(end >= begin, "end must be >= m");

    using array_type = std::array<std::string_view, end - begin + 1>;

    template <std::size_t ... Is>
    static constexpr array_type make_mapping(std::index_sequence<Is...>)
    {
        array_type result{};
        ((result[Is] = enum_value_name<static_cast<Enum>(begin + Is)>()), ...);
        return result;
    }

    static inline constexpr array_type mapping = make_mapping(std::make_index_sequence<array_type{}.size()>{});
};

Insufficient macro sanitization

Part of why macros are so evil is that they can be used anywhere, even in namespaces where someone has defined their own namespace xyz::enum_stringifyer or namespace xyz::std. All non-local symbols should be accessed using their fully qualified names:

#define DEFINE_ENUM_STRINGIFYER_RANGE(Enum, min_value, max_value)                          \
std::ostream& operator <<(std::ostream& os, const Enum v)                                  \
{                                                                                          \
    static_assert(::std::is_same_v<decltype(min_value), Enum>, "min_value must be "#Enum); \
    static_assert(::std::is_same_v<decltype(max_value), Enum>, "max_value must be "#Enum); \
    using mapper = ::enum_stringifyer::EnumMapperRange<Enum, min_value, max_value>;        \
    if (::enum_stringifyer::to_underlying(v) < mapper::begin                               \
     || ::enum_stringifyer::to_underlying(v) > mapper::end)                                \
    {                                                                                      \
        return os << '(' << #Enum << ')' << static_cast<int>(v);                           \
    }                                                                                      \
    return os << mapper::mapping[::enum_stringifyer::to_underlying(v) - mapper::begin];    \
} static_assert(true, "") /* To require a ; after macro call */

Fundamental Redesign

As we go further down in the code, we end up in a macro hell where operator overloads are defined in macros. Macros should generally be minimal crutches that help us deal with minor problems that templates cannot overcome.

The implementation of operator<< doesn't need to be a macro at all. We can write the following:

template <typename Enum>
auto operator<<(std::ostream& os, const Enum v)
    -> std::enable_if_t<std::is_enum_v<Enum> && enum_stringifyer::has_enum_traits<Enum>::value, std::ostream&>
{
    using namespace enum_stringifyer;
    if constexpr (has_enum_traits_list<Enum>::value) {
        static constexpr auto names = make_listed_names<Enum>();
    
        for (std::size_t i = 0; i < std::size(enum_traits<Enum>::list); ++i) {
            if (v == enum_traits<Enum>::list[i]) {
                return os << names[i];
            }
        }
    }
    else if constexpr (has_enum_traits_range<Enum>::value) {
        static constexpr auto names = make_range_names<Enum>();
        constexpr auto begin = to_underlying(enum_traits<Enum>::begin);
        constexpr auto end = to_underlying(enum_traits<Enum>::end); 
        
        auto underlying = to_underlying(v);
        if (underlying >= begin && underlying <= end)
        {
            return os << names[underlying - begin];
        }
    }
    
    // zero_name should be Enum::ZERO, or (Enum)0
    static constexpr std::string_view zero_name = enum_value_name<Enum{0}>();
    static constexpr std::string_view fallback_name = enum_value_enum_name(zero_name);

    return os << '(' << fallback_name << ')' << +to_underlying(v);
}

We need the following to make this happen:

  • create an enum_traits template which acts as a customization point, similar to std::iterator_traits
  • add some "member detector idioms" to detect what static members the user has defined in the traits
  • refactor the stringification of name lists and name ranges into function templates:
    • make_listed_names which converts the list of enums into a list of std::string_view
    • make_range_names which converts the range of enums into a list of std::string_view
  • an enum_value_enum_make function that maps Enum::ZERO onto Enum

The customization on the user's end looks like this:

template <>
struct enum_stringifyer::enum_traits<Test> {
    static inline constexpr Test begin = Test::neg;
    static inline constexpr Test end = Test::b;
};

and

template <>
struct enum_stringifyer::enum_traits<Test2> {
    static inline constexpr std::array list = {
        Test2::a,
        Test2::b,
        Test2::c,
        Test2::d,
        Test2::e,
        Test2::g,
        Test2::h,
        Test2::i,
        Test2::j
    };
};

See live implementation

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the detailed answer! About your redesign, I think it comes close to what magic-enum library does (at least the user customization looks similar). However, I think that right now it fails my last requirements as I wouldn't have ever been able to produce such code ^^'. I'll need to spend a lot of time reading it to understand and learn how it works. But it's more a me problem than your code's fault! \$\endgroup\$
    – adepierre
    Jun 17, 2023 at 16:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.