Getting an Unmangled Type Name at Compile Time
A really useful and obscure technique
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!
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:
- Find where the prefix starts, and get the index at the end of it
- Find where the suffix ends, and get the index at the beginning of it
- 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>
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@
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
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 #ifdef
s).
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()};
}
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
).