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