Abnormal programming

A C++ enum's name at runtime

Nov 7, 202210 min

Getting a type's name — whether it's a struct or an enum — in C++ is a problem. What is trivially known to the compiler while it parses the sources can't easily be turned into a human-readable form at runtime. You can use std::type_info::name, which isn't a portable solution because it depends on the compiler's name-mangling implementation. Some compilers (e.g. MSVC, IBM, Oracle) produce a "convenient" type name, while gcc and clang return a mangled name that can be turned into a readable form with extra functions such as abi::__cxa_demangle. For all that magic to work you need RTTI enabled, which isn't always available either — and is sometimes outright harmful, because it eats precious performance.

A C++ enum's name at runtime

The C++ language has no real form of reflection; only in C++20 did proposals appear (reflexpr) to add native facilities for it. Here's roughly how clang reports class data.

Show code
#include <iostream>
#include <typeinfo>
#include <cxxabi.h>

struct Simple { };
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
enum Enum { one = 4, two = 2 };

int main() {
    Base b1;
    Derived d1;
    Simple c1;
    Enum eenum;

    const Base *pb = &b1;
    std::cout << typeid(*pb).name() << '\n';
    pb = &d1;
    std::cout << typeid(*pb).name() << '\n';

    std::cout << typeid(eenum).name() << '\n';

    std::cout << typeid(c1).name() << '\n';

    int     status;
    char   *realname;

    realname = abi::__cxa_demangle(typeid(*pb).name(), 0, 0, &status);
    std::cout << typeid(*pb).name() << "\t=> " << realname << "\t: " << status << '\n';
}

$> 4Base
   7Derived
   4Enum
   6Simple
   7Derived        => Derived      : 0

So the compiler keeps all this data in memory, and we can use it — pull it out and process it however we like.

Can we get the type via a macro?

In principle yes, but it won't be very useful. To repeat: there are no explicit language constructs that give a type's name outside of typeid, so we have to look for other options. The simplest solution is to stringify the type itself.

Show code
#define TYPE_NAME(x) #x
std::cout << TYPE_NAME(std::string) << std::endl;

You can guess, of course, what gets printed to the console — only it works for explicitly named types. A macro doesn't care what it stringifies, an elephant or a pug. So it already breaks on a slightly more complex example.

Show code
template <typename T>
void print(int x) {
  std::cout << TYPE_NAME(T) << std::endl;
  std::cout << TYPE_NAME(decltype(x)) << std::endl;
}

$> T
   decltype(x)

Can we get the type via a template?

Show code
template <typename T>
void bad_function();

If you've ever made a typo in a template, you've surely seen a giant wall of error text that ended with something about bad_function<foo>() — i.e. the compiler pulled out both the template function's name and the name of its parameter.

There was a joke about C++ here.

There was a joke about C++ here.

Can this work outside compiler error output? While investigating how to pull out a type's name, an enum's name, or an enum value's name at compile time, I stumbled onto the service variable __function__. What does the documentation say about it? The compiler fills its contents, in a form convenient to itself, at the point of declaration — e.g. for error or warning output.

Documentation
__FUNCTION__ Defined as a string literal that contains the undecorated name of the
enclosing function. The macro is defined only within a function. The __FUNCTION__
macro isn't expanded if you use the /EP or /P compiler option. For an example of
usage, see the __FUNCNAME__ macro.

__PRETTY_FUNCTION__ is a gcc extension that is mostly the same as __FUNCTION__,
except that for C++ functions it contains the "pretty" name of the function including
the signature of the function. Visual C++ has a similar (but not quite identical)
extension, __FUNCSIG__ .

(n1642) This is a hidden variable that exists inside the call site as a const char*. It also contains extra information about overloads and templates — let's see what we can do with it.

Show code
struct Simple { };

template <typename T>
std::string test() {
  return __PRETTY_FUNCTION__;
}

int main() {
    std::cout << test<std::string>() << std::endl;
    std::cout << test<Simple>() << std::endl;
}

$> std::string test() [with T = std::__cxx11::basic_string<char>; std::string = std::__cxx11::basic_string<char>]
   std::string test() [with T = Simple; std::string = std::__cxx11::basic_string<char>]

Now we have something to work with. Let's try writing a simple parser that extracts the data we need from this string. For that we can use the string class's substring-search functions. The search looks a bit heavy and doesn't work at compile time yet, but we'll get there.

Show code
struct Simple { };

template <typename T>
struct TypeNameHelper {
    static std::string TypeName(void) {
        const auto prefix   = std::string{"[with T = "};
        const auto suffix   = std::string{";"};
        const auto function = std::string{__PRETTY_FUNCTION__};

        const auto start    = function.find(prefix) + prefix.size();
        const auto end      = function.rfind(suffix);

        const auto result   = function.substr(start, (end - start));

        return result;
    }
};

template <typename T>
std::string TypeName(void) {
    return TypeNameHelper<T>::TypeName();
}

$> std::__cxx11::basic_string<char>
Simple

It's also worth making sure the compiler doesn't strip the full function name when building with optimizations, otherwise we may get empty strings with -O2/3. With optimizations all the strings are in place; with -O3 (example) too. Let's tweak the code a little so we can inspect the output for different data types and save it for later use.

Show code
enum test_enum { te_first, te_second, te_third, te_count };
int main() {
   std::cout << TypeName<test_enum>() << std::endl;
}

$> test_enum

Now the output has a readable enum type; all that's left is to pull out its value. For that we add one more parameter to the template — the enum value. There's no template magic yet, that comes a bit later.

Show code
template <typename T, T V>
struct TypeNameHelper {
    static const char* TypeName(void) {
        static const size_t size = sizeof(__PRETTY_FUNCTION__);
        static char typeName[size] = {};
        memcpy(typeName, __PRETTY_FUNCTION__ , size - 1u);
        return typeName;
    }
};

template <typename T, T V>
const char* TypeName(void) {
    return TypeNameHelper<T, V>::TypeName();
}

enum test_enum { te_first, te_second, te_third, te_count };
int main() {
   std::cout << TypeName<test_enum, te_second>() << std::endl;
}

$> test_enum; T V = te_first

Now, coming back to the task in the title — besides the type itself we also need all the values. To build a sequence and turn it into an array you usually use std::integer_sequence and fold expressions; that's how you can, for example, fill an array with the natural numbers. Of course this breaks a little if the enum has "holes".

Show code
template<typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> int_seq) {
    std::cout << "The sequence of size " << int_seq.size() << ": ";
    ((std::cout << ints << ' '), ...);
    std::cout << '\n';
}

int main() {
     print_sequence(std::make_integer_sequence<int, 20>{});
}

$> The sequence of size 20: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Pulling all the thoughts together, we start sketching out a possible solution:

  1. expand the enum from the first to the last element into an array via std::integer_sequence;
  2. walk the resulting array and call the parameterized TypeName for each element, saving or processing the output;
  3. build a set of { name, value } structs to iterate over the elements.

Expanding the enum values into an array (easy)

So as not to bore the reader with the design and debugging, I'll just post the finished classes and explain individual points. As it turned out, convincing the compiler to turn an enum into a string isn't so much hard as slow — in terms of threading parameters through a dozen templates-of-templates. In the end I got it working, but only for clang and Visual Studio; with gcc, alas, it didn't work out.

Show code
struct token {
    const char * name;
    s32 id;
};

template<typename enum_type, enum_type begin, enum_type end>
struct token_holder {
    constexpr static inline int count_values(enum_type type) {
        return (type != end)
            ? count_values((enum_type)(s32(type)+1))
            : (s32(type)+1);
    }

    static constexpr u32 N = count_values(begin);
    using tokens = std::array<token, N+1>; // because have {0, 0} in last element

    template<typename e_enum_type, e_enum_type enum_value>
    constexpr token make_name() {
        return { enum_name<e_enum_type, enum_value>().name(), enum_value };
    }

    template<typename e_enum_type, u32... Indices>
    constexpr tokens make_tokens_helper(std::integer_sequence<u32, Indices...>) {
        tokens ts = { make_name<e_enum_type, (e_enum_type)Indices>()... };
        ts[N] = {0, 0};
        return ts;
    }

    template<typename e_enum_type>
    constexpr tokens make_tokens() {
        return make_tokens_helper<e_enum_type>( std::make_integer_sequence<u32, N>{});
    }


    constexpr operator const token *() const { return values.unsafe_ptr(); }
    constexpr token *data() const { return values.unsafe_ptr(); }
    constexpr token_holder() : values{make_tokens<enum_type>()} {}

    tokens values;
};

Turning an enum value into a string (a bit harder)

Let's look at what a template-wrapped enum turns into, for example something like this

Compiler output
static const char *enum_name<test_enum, te_first>::helper_name()
  [enum_type = test_enum, enum_value = te_first, value = te_first]

and we look for the last occurrence of the "=" character and copy everything to the end. The static qualifiers here are needed so the values stay in memory after the function returns.

Show code
template<typename enum_type, enum_type enum_value>
class enum_name final {
    // Helper function that directly takes an enum_type value template parameter to get the name of the classes template enum type value as a string.
    template <enum_type value>
    static const char* helper_name() {
        // Clang compiler format:   __PRETTY_FUNCTION__
        // = static const char *enum_name<ENUM_TYPE_NAME, ENUM_TYPE_NAME::ENUM_VALUE_NAME>::helper_value() [enum_type = ENUM_TYPE_NAME, enum_value = ENUM_TYPE_NAME::ENUM_VALUE_NAME, value = ENUM_TYPE_NAME::ENUM_VALUE_NAME]
        // static const char *enum_name<test_enum, te_first>::helper_name() [enum_type = test_enum, enum_value = te_first, value = te_first]

        // MSVC compiler format:    __FUNCSIG__
        // = const char *__cdecl enum_name<enum ENUM_TYPE_NAME,ENUM_VALUE>::helper_value<ENUM_TYPE_NAME::ENUM_VALUE_NAME>(void)
        // const char *__cdecl enum_name<enum test_enum,0>::helper_name<te_first>(void)

        static const unsigned long long function_name_length = sizeof(__PRETTY_FUNCTION__);
        static const std::string function_name(__PRETTY_FUNCTION__);
        static const unsigned long long type_name_start  = function_name.rfind('=') + 2;
        static const unsigned long long type_name_end    = function_name_length - 2;
        static const unsigned long long type_name_length = type_name_end - type_name_start;
        static const std::string type_name_string = function_name.substr((size_t)type_name_start, (size_t)type_name_length);
        return type_name_string.c_str();
    }

public:
    static const char* name() { return helper_name<enum_value>(); }				// Get the name of the classes template enum type and value as a string.
};

In this form the code already prints enum values (example).

Printing the enum values

Getting rid of string (not too hard)

You could do it with std::string_view, but that would be less interesting. From the string class we only needed character storage, rfind and substr. __PRETTY_FUNCTION__ exists at compile time, so we can write a small service class that a) stores the string and b) does no dynamic allocation, plus write the functions we need to work with a substring. memcpy or loops in constexpr won't work for copying and searching data, but a loop can be turned into recursion with a parameter, implementing rfind that way (it's compile time — who's counting the time spent expanding the template). substr, though, I couldn't manage; I had to google a solution (compile-time substring) and look at how std::string_view does it. And of course we add c_str() — where would C++ be without that legendary function. constexpr in the calls is needed so the compiler can compute the array sizes when instantiating templates, at the call sites at compile time.

Show code
template <unsigned long long length>
class string_literal final {
	const char string[length];

public:
	template<unsigned long long... indexes>
  	const string_literal(const char(&raw_string)[sizeof...(indexes)], std::integer_sequence<unsigned long long, indexes...> index_sequence) : string{ raw_string[indexes]... } {
		static_cast<void>(index_sequence);
	}

	constexpr const char* c_str() const { return this->string; }

	template<unsigned long long substring_start, unsigned long long substring_length, unsigned long long... substring_indexes>
	constexpr const string_literal<substring_length + 1> substr(std::integer_sequence<unsigned long long, substring_indexes...> substring_index_sequence) const {
		static_cast<void>(substring_index_sequence);
		return string_literal<substring_length + 1>({ this->string[substring_start + substring_indexes]..., '\0' }, std::make_integer_sequence<unsigned long long, substring_length + 1>{});
	}

	constexpr unsigned long long find(const char character, const unsigned long long starting_index = 0) const {
		return						((starting_index >= length)
										? 0xFFFFFFFFFFFFFFFF
										: ((this->string[starting_index] == character)
											? starting_index
											: this->find(character, starting_index + 1)));
	}

	constexpr unsigned long long rfind(const char character, const unsigned long long starting_index = length - 1) const {
		return				((starting_index == 0)
								? ((this->string[starting_index] == character)
									? starting_index
									: 0xFFFFFFFFFFFFFFFF)
  							    : ((this->string[starting_index] == character)
									? starting_index
									: this->rfind(character, starting_index - 1)));
	}
};

Now, putting all the puzzle pieces together, we can write a solution that turns an enum into a string. The output is an array you can iterate over to find the value you need, and it also holds the string representation of its name.

Show code
enum test_enum { te_first, te_second, te_third, te_count };

int main() {
    const token_holder<test_enum, te_first, te_count> test_tokens;

    for (auto &s: test_tokens.values)
        std::cout << "{" << (s.name ? s.name : "null")
                  << ", " << s.id << " }"
                  << std::endl;
}

$>  {te_first, 0 }
    {te_second, 1 }
    {te_third, 2 }
    {te_count, 3 }
    {null, 0 }

Instead of conclusions

No cheating, only black magic. Although there may be no built-in way to get an object's full type name at compile time, you can hack the language itself (I love C++ for letting you pull off this kind of madness) to reach the result you want. Was it useful, and will I drag it into production — hardly; was it interesting — absolutely. If you need a ready-made solution, look up magic_enum on GitHub, but it needs C++17 to work, whereas I'm not done roaming the lands of the C++11/14 dragons yet.

P.S. the code is here — github.com/dalerank/tokenum.

← All articles