Getting an Unmangled Type Name at Compile Time

A really useful and obscure technique

  • 10 minutes to read

Getting the name of a type in C++ is a hassle. For something that should be trivially known by the compiler at compile-time, the closest thing we have to getting the type in a cross-platform way is to use std::type_info::name which is neither at compile-time, nor is it guaranteed to be human-readable.

In fact, both GCC and Clang actually return the compiler’s mangled name rather than the human-readable name we are used to. Let’s try to make something better using the modern utilities from c++17 and a little creative problem solving!

Reflecting on the Type Name

The C++ programming language lacks any real form of reflection, even all the way up to c++20. Some reflection-like functionalities can be done by leveraging strange or obscure techniques through templates; but overall these effects tend to be quite limited.

So how can we get the type name without reflection?

Realistically, we shouldn’t actually need reflection for this purpose; all we need is something the compiler can give us that contains the type name in a string. As long as the string is known at compile time, we can then use modern C++ utilities to parse the string to deliver it to us also at compile-time.

Finding a String at Compile Time

We know that typeid/std::type_info::name is out – since this doesn’t operate at compile-time or yield us a reasonable string. There aren’t any specific language constructs explicitly giving us the type name outside of typeid, so we will have to consider other sources of information.

Checking the C++ standard, the only other sources of strings that may exist at compile-time come from the preprocessor and other builtins; so lets start there.

Macros

One somewhat obvious approach to getting a type name as a string is to leverage the macro preprocessor to stringize the input. For example:

#define TYPE_NAME(x) #x

This will work for cases where the type is always statically expressed, such as:

std::cout << TYPE_NAME(std::string) << std::endl;

which will print

std::string

But it falls short on indirect contexts where you only have the T type, or you are deducing the type from an expression! For example:

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

will print

T
decltype(x)

Which is not correct. So the preprocessor is unfortunately not an option here. What else could we use?

Perhaps we could extract something that contains the type name in the string – like getting the name of a function?

Function Name

If we had a function that contains the type we want to stringize; perhaps something like:

template <typename T>
void some_function();

Where a call of some_function<foo>() would produce a function name of "some_function<foo>()", then we could simply parse it to get the type name out. Does C++ have such a utility?

Standard C++ offers us a hidden variable called __func__ , which behaves as a constant char array defined in each function scope. This satisfies the requirement that it be at compile-time; however its notably very limited. __func__ is only defined to be the name of the function, but it does not carry any other details about the function – such as overload information or template information, since __func__ was actually inherited from C99 which has neither.

However, this doesn’t mean its the end. If you check in your trusty compiler manual(s), you will find that all major compilers offer a __func__ equivalent for C++ that also contains overload and template information!

Exploiting the Names of Functions

GCC and Clang both offer a __func__-like variable for C++ which contains more detailed information as an extension called __PRETTY_FUNCTION__ . MSVC also offers a similar/equivalent one called __FUNCSIG__ .

These act as compiler extensions and as such are not – strictly speaking – portable; however this does not mean that we can’t wrap this into a useful way. Lets try to make a proof-of-concept using GCC’s __PRETTY_FUNCTION__.

Format

The first thing we need to know is what GCC’s format is when printing a __PRETTY_FUNCTION__. Lets write a simple test:

template <typename T>
auto test() -> std::string_view
{
  return __PRETTY_FUNCTION__;
}

...

std::cout << test<std::string>();

Yields

std::string_view test() [with T = std::__cxx11::basic_string<char>; std::string_view = std::basic_string_view<char>]

Which means that, at least for GCC, we need to find where with T = begins and ; ends to get the type name in between!

Try Online

Parsing: A first attempt

So lets try to write a simple parser that works at compile-time. For this we can use <string_view> for the heavy-lifting.

template <typename T>
constexpr auto type_name() -> std::string_view
{
  constexpr auto prefix   = std::string_view{"[with T = "};
  constexpr auto suffix   = std::string_view{";"};
  constexpr auto function = std::string_view{__PRETTY_FUNCTION__};

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

  static_assert(start < end);

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

  return result;
}

The algorithm is simple:

  1. Find where the prefix starts, and get the index at the end of it
  2. Find where the suffix ends, and get the index at the beginning of it
  3. Create a substring between those two indices

Lets test if it works. A simple program of:

std::cout << type_name<std::string>() << std::endl;

now prints:

std::__cxx11::basic_string<char>
Try Online

Great! Before we celebrate, we should check to make sure that the compiler isn’t embedding the entire function name – otherwise we might bloat the executable with unused strings. Otherwise, this would be quite the trade-off, and not zero-overhead.

Checking the generated assembly, we can see that the string does exist in its complete form, even at -O3:

type_name<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >()::__PRETTY_FUNCTION__:
        .string "constexpr std::string_view type_name() [with T = std::__cxx11::basic_string<char>; std::string_view = std::basic_string_view<char>]"

Which means that the compiler was unable to detect that the rest of the string name was unused.

Lets fix this.

Iteration: Optimize out unused string segments

In the first approach, the string is taken in its entirety – which is likely why its also seen in the assembly verbatim. The compiler sees its used, but is unable to see that only part of it is used. So how can we make it see that?

The easiest way is to instead build a string, at compile-time, that only contains the name of the type – and not the rest of __PRETTY_FUNC__. This way the compiler will not see any runtime uses of the function name, it will only see runtime uses of the manually built string.

Unfortunately there is no way to build a constexpr string specifically in C++17, and basic_fixed_string never made it into C++20; so we will have to do this the old fashioned way: with a char array!

To build the array, we will need to extract each character independently at compile-time at each specific index. This is a job for std::index_sequence, and we can leverage C++17’s CTAD of std::array along with auto-deduced return types to make this even easier:

// Creates a std::array<char> by building it from the string view
template <std::size_t...Idxs>
constexpr auto substring_as_array(std::string_view str, std::index_sequence<Idxs...>)
{
  return std::array{str[Idxs]..., '\n'};
}

And then we just need to update our type_name() function to make use of this

template <typename T>
constexpr auto type_name() -> std::string_view
{
  ...

  static_assert(start < end);

  constexpr auto name = function.substr(start, (end - start));
  constexpr auto name_array = substring_as_array(name, std::make_index_sequence<name.size()>{});

  return std::string_view{name_array.data(), name_array.size()};
}

Lets test to see if it works! type_name<std::string>() now gives us:

�@�o�4P@
Try Online

Un-oh, something definitely went wrong!

If we look closely at the previous code, we are actually returning a reference to a local constexpr variable – creating a dangling reference:

  constexpr auto name_array = substring_as_array(name, std::make_index_sequence<name.size()>{});
  //        ^~~~~~~~~~~~~~~ <-- This is local to the function

  return std::string_view{name_array.data(), name_array.size()};

What we would like is for name_array to be static; though unfortunately constexpr requirements in C++ prevents objects with static storage duration from being defined within constexpr functions.

Fixing our dangling pointer

Though we can’t define a static storage duration object inside of a constexpr function, we can define a constexpr static storage duration object from a constexpr function – as long as its a static class member.

template <typename T>
struct type_name_holder {
  static inline constexpr auto value = ...;
};

If we rework our code from before a little bit, we can rearrange it so that the parsing from the type_name function now returns the array of characters at constexpr time to initialize the type_name_holder<T>::value object.

// Note: This has been renamed to 'type_name_array', and now has the return
// type deduced to simplify getting the array's size.
template <typename T>
constexpr auto type_name_array()
{
  ...
  // Note: removing the return type changes the format to now require ']' not ';'
  constexpr auto suffix   = std::string_view{"]"};

  ...

  static_assert(start < end);

  constexpr auto name = function.substr(start, (end - start));
  // return the array now
  return substring_as_array(name, std::make_index_sequence<name.size()>{});
}

template <typename T>
struct type_name_holder {
  // Now the character array has static lifetime!
  static inline constexpr auto value = type_name_array<T>();
};

// The new 'type_name' function
template <typename T>
constexpr auto type_name() -> std::string_view
{
  constexpr auto& value = type_name_holder<T>::value;
  return std::string_view{value.data(), value.size()};
}

Trying this again now, we get the proper/expected output:

std::__cxx11::basic_string<char>

And checking the assembly, we now see a sequence of binary values representing only the type name – and not the whole __PRETTY_FUNCTION__

type_name_holder<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::value:
        .byte   115
        .byte   116
        .byte   100
        .byte   58
        .byte   58
        .byte   95
        .byte   95
        .byte   99
        .byte   120
        .byte   120
        .byte   49
        .byte   49
        .byte   58
        .byte   58
        .byte   98
        .byte   97
        .byte   115
        .byte   105
        .byte   99
        .byte   95
        .byte   115
        .byte   116
        .byte   114
        .byte   105
        .byte   110
        .byte   103
        .byte   60
        .byte   99
        .byte   104
        .byte   97
        .byte   114
        .byte   62
        .byte   10
Try Online

So yay, we got it working for GCC!

Supporting other compilers

Clang and MSVC can be handled similarly, and only require the prefix, suffix, and function variables to be changed at compile-time (e.g. through #ifdefs).

In the end, I came up with the following snippet to toggle the behavior:

template <typename T>
constexpr auto type_name_array()
{
#if defined(__clang__)
  constexpr auto prefix   = std::string_view{"[T = "};
  constexpr auto suffix   = std::string_view{"]"};
  constexpr auto function = std::string_view{__PRETTY_FUNCTION__};
#elif defined(__GNUC__)
  constexpr auto prefix   = std::string_view{"with T = "};
  constexpr auto suffix   = std::string_view{"]"};
  constexpr auto function = std::string_view{__PRETTY_FUNCTION__};
#elif defined(_MSC_VER)
  constexpr auto prefix   = std::string_view{"type_name_array<"};
  constexpr auto suffix   = std::string_view{">(void)"};
  constexpr auto function = std::string_view{__FUNCSIG__};
#else
# error Unsupported compiler
#endif
  ...
}

These three variables are the only ones that would need to be changed to port to a different compiler that also offers some form of __PRETTY_FUNCTION__-like equivalent.

A Working Solution

Putting it all together, our simple utility should look like:

#include <string>
#include <string_view>
#include <array>   // std::array
#include <utility> // std::index_sequence

template <std::size_t...Idxs>
constexpr auto substring_as_array(std::string_view str, std::index_sequence<Idxs...>)
{
  return std::array{str[Idxs]..., '\n'};
}

template <typename T>
constexpr auto type_name_array()
{
#if defined(__clang__)
  constexpr auto prefix   = std::string_view{"[T = "};
  constexpr auto suffix   = std::string_view{"]"};
  constexpr auto function = std::string_view{__PRETTY_FUNCTION__};
#elif defined(__GNUC__)
  constexpr auto prefix   = std::string_view{"with T = "};
  constexpr auto suffix   = std::string_view{"]"};
  constexpr auto function = std::string_view{__PRETTY_FUNCTION__};
#elif defined(_MSC_VER)
  constexpr auto prefix   = std::string_view{"type_name_array<"};
  constexpr auto suffix   = std::string_view{">(void)"};
  constexpr auto function = std::string_view{__FUNCSIG__};
#else
# error Unsupported compiler
#endif

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

  static_assert(start < end);

  constexpr auto name = function.substr(start, (end - start));
  return substring_as_array(name, std::make_index_sequence<name.size()>{});
}

template <typename T>
struct type_name_holder {
  static inline constexpr auto value = type_name_array<T>();
};

template <typename T>
constexpr auto type_name() -> std::string_view
{
  constexpr auto& value = type_name_holder<T>::value;
  return std::string_view{value.data(), value.size()};
}
Try Online

Closing Thoughts

Although there may not be any built-in facility to get the full type-name of an object at compile-time; we can easily abuse other features of C++ to make this possible in a reasonably portable way.

This is all possible thanks to the many reduced restrictions on constexpr functions, and thanks to a rich feature-set of constexpr functionality in the standard library (particularly string_view).

Next Post