Constexpr std::type_info::name
typeid and std::type_info are an utility from the standard for getting information about types at runtime.
One particular feature I’m interested is, is to get a string representation of a given type.
But doing such operation at runtime looks like a code smell for my use-cases; the type is often known at compile-time, shouldn’t it be possible to get the string at compile-time too?
With macros, it is extremely easy to implement something like that. So easy, and yet broken in a lot of scenarios:
#include <cstdio>
#define TOSTRING(X) #X
template<class T>
void bar(){
std::puts(TOSTRING(T));
}
struct X1{};
namespace fek{
struct X2{};
using X3 = X2;
void foo(){
std::puts(TOSTRING(X1)); // correct
std::puts(TOSTRING(x1)); // typo, x1 is not a type
std::puts(TOSTRING(X2)); // prints X2 instead of fek::X2
std::puts(TOSTRING(X3)); // prints bar instead of fek::X2
bar<X1>(); // prints T instead of X1
bar<X2>(); // prints T instead of fek::X2
bar<X3>(); // prints T instead of fek::X2
std::puts(TOSTRING(decltype(1))); // prints decltype(1) instead of int
}
} One could avoid the typo, for example with std::is_class_v and similar traits, at least that would be covered.
There is no way to cover aliases, which for my use-cases is only a minor issue, maybe even a feature, but there is no way to cover template<class T> void bar() and decltype, which is a major issue.
Unfortunately, the C++ standard does not offer such feature.
Use-cases
The main use-case might not seem that interesting: logging.
I have two mainly two examples, one where the type is known statically, and another one where it is queried dynamically.
#include <memory>
#include <iostream>
struct base{
virtual ~base() = default;
};
struct derived1 : base{};
struct derived2 : base{};
std::unique_ptr<base> get(bool b){
return b ? std::unique_ptr<base>(std::make_unique<derived1>())
: std::unique_ptr<base>(std::make_unique<derived2>());
}
void foo(bool b){
std::unique_ptr<base> ptr = get(b);
std::cout << "name: " << typeid(*ptr).name(); // prints derived1 or derived2 (mangled)
} struct myclass{
void foo(){
// ...
std::cout << typeid(*this).name(); // prints myclass (mangled)
// ...
}
}; In the second example, one could hard-code the string, or even use the macro TOSTRING with myclass as parameter. This is error-prone as one needs to update the parameter of TOSTRING if the class is moved around or renamed.
If it where possible to determine the type at compile-time, it cannot support the first example as-is, as the type of *ptr is of type base, and not the derived class.
This is not necessarily a major issue; in case of dynamic types, the string representation would be an additional return value of get(), or a member variable of base. It is obviously not a viable solution if you cannot change get() or base.
Pretty functions
GCC offer the macro __PRETTY_FUNCTION__, which contains the signature of the function as well as its bare name, as the documentation 🗄️ states:
class a {
public:
void sub (int i) {
std::puts(__PRETTY_FUNCTION__);
}
};
int main() {
a ax;
ax.sub(0); // prints void a::sub(int)
return 0;
} Most importantly, __PRETTY_FUNCTION__ also contains template parameters, as following example shows
template <typename T>
std::string_view test() {
return __PRETTY_FUNCTION__;
}
int main(){
std::cout << test<int>(); // prints const char* test() [with T = int]
} Clang also supports __PRETTY_FUNCTION__, although the output looks slightly different: const char *test() [T = int].
MSVC does not support __PRETTY_FUNCTION__ but provides __FUNCSIG__.
Note that with some preprocessor flags, __FUNCSIG__ is empty 🗄️. I wonder if this can cause issues with tools like ccache:
__FUNCSIG__Defined as a string literal that contains the signature of the enclosing function. The macro is defined only within a function. The__FUNCSIG__macro isn’t expanded if you use the /EP or /P compiler option. […]
Parse the generated string - at compile time
Just search for prefix and suffix, and return what remains to the caller.
To keep the example minimal, the output will be stored in a global array which can be accessed through std::string_view.
#include <iostream>
#include <string>
#include <string_view>
#include <array>
#include <utility>
// NOTE: use auto instead of std::string_view in signature
// makes parsing with GCC and MSVC easier, otherwise one has to strip the return type from the signature
template <typename T>
consteval auto type_name() {
#if defined(__GNUC__) and !defined(__clang__)
constexpr auto prefix = std::string_view(" [with T = ");
constexpr auto suffix = std::string_view("]");
constexpr auto function = std::string_view(__PRETTY_FUNCTION__);
#elif 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(_MSC_VER)
constexpr auto prefix = std::string_view("<");
constexpr auto suffix = std::string_view(">(void)");
constexpr auto function = std::string_view(__FUNCSIG__);
#else
# error Unsupported compiler
#endif
static_assert(not function.empty());
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));
constexpr static auto res = [] <std::size_t...Idxs>(std::string_view str, std::index_sequence<Idxs...>) {
return std::array{str[Idxs]...};
}(name, std::make_index_sequence<name.size()>{});
return std::string_view{res.data(), res.size()};
}
namespace bar{struct foo{};}
using foo = bar::foo;
int main() {
std::cout << "name: " << type_name<foo>() << " 1\n";
} Success!
Compiler bugs and older standard
Unfortunately, Clang has issues when using consteval, but works as expected with constexpr.
There is already an open bug report, and this is a good reminder that testing at compile-time is a powerful feature, but not enough, just like unit tests are not a replacement for end to end tests.
There are also a couple of other things to consider:
-
defining a
staticvariable in a constexpr function is a C++23 feature -
explicit template parameter list for lambdas is a C++20 feature
-
constevalis a C++20 feature
If using std::string_view is an issue (available since C++17), it should be the last of your concerns, as it can be implemented easily for older standards, but the other mentioned features require a different type of implementation. Fortunately none of them are required; but they help to make the code more compact.
A C++17 compatible version would look like the following:
#include <string_view>
#include <array>
#include <utility>
template <std::size_t...Idxs>
constexpr auto substring_as_array(std::string_view str, std::index_sequence<Idxs...>) {
return std::array{str[Idxs]...};
}
template <typename T>
constexpr auto type_name_helper() {
#if defined(__GNUC__) and !defined(__clang__)
constexpr auto prefix = std::string_view(" [with T = ");
constexpr auto suffix = std::string_view("]");
constexpr auto function = std::string_view(__PRETTY_FUNCTION__);
#elif 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(_MSC_VER)
constexpr auto prefix = std::string_view("<");
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 array = type_name_helper<T>();
};
template <typename T>
constexpr std::string_view type_name() {
return std::string_view(type_name_holder<T>::array.begin(), type_name_holder<T>::array.size());
}
namespace bar{struct foo{};}
using foo = bar::foo;
int main() {
std::cout << "name: " << type_name<foo>() << " 1\n";
} Extra features
Not all types might output what one would expect:
#include <iostream>
int main(){
auto l = [](int){};
std::cout << type_name<decltype(l)>() << "\n" << type_name<std::string>();
} Output of CLang:
(lambda at /app/example.cpp:10:12)
std::basic_string<char> Output of GCC:
main()::<lambda(int)>
std::__cxx11::basic_string<char> Output of MSVC:
class main::<lambda_1>
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > Instead of (lambda at /app/example.cpp:10:12), a better output would be (lambda) or another user-defined literal for the lambda. It would also have the advantage that the output does not depend from the source location, making builds slightly more reproducible.
For std::string, a better output would be std::string, instead of the aliased name, especially when dealing with containers, the output of something like std::map<std::string, std::pait<std::string, std::string> can be quite difficult to parse for the reader, and is unnecessarily long.
Also the class and struct in the MSVC output are unnecessary, it would make sense to remove them too.
One could implement such "pretty-printing" features in type_name directly, and for types of the standard library it might make sense.
But at that point, it might make more sense to have more functions for handling type names. For example, one might prefer to use an aliased name (just like for std::string), get the type without the namespace, and so on. If it makes sense to support such features, then it would be better if type_name returns an owning container instead of creating a global variable holding the array, so that the caller can modify it as desired and store the result somewhere.
Also, since std::string can be used in a constexpr context, it might be simpler to return std::string, and let the caller copy the content in an array.
A possible implementation with some pretty-printing at compile-time could look like the following:
#include <string_view>
#include <string>
#include <array>
#include <iostream>
#if defined(_MSC_VER)
#define LAMBDA_CONSTEVAL
#else
#define LAMBDA_CONSTEVAL consteval
#endif
#if defined(_MSC_VER) || defined(__clang__)
// see https://github.com/llvm/llvm-project/issues/82994 for clang
// msvc currently fails to compile with consteval instead of constexpr
#define CONSTEVAL_STATIC constexpr
#else
#define CONSTEVAL_STATIC consteval
#endif
template <typename T>
constexpr auto type_name() {
#if defined(__GNUC__) and !defined(__clang__)
constexpr auto prefix = std::string_view( " [with T = " );
constexpr auto suffix = std::string_view( "]" );
constexpr auto function = std::string_view( __PRETTY_FUNCTION__ );
#elif defined(__clang__)
constexpr auto prefix = std::string_view( " [T = " );
constexpr auto suffix = std::string_view( "]" );
constexpr auto function = std::string_view( __PRETTY_FUNCTION__ );
// workaround for clang, see https://github.com/llvm/llvm-project/issues/142144
static_assert(std::string("test").size() >1);
#elif defined(_MSC_VER)
constexpr auto prefix = std::string_view( "<" );
constexpr auto suffix = std::string_view( ">(void)" );
constexpr auto function = std::string_view( __FUNCSIG__ );
#else
# error "Unsupported compiler"
#endif
static_assert( not function.empty() );
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 ) );
auto prettify = []( std::string sTarget ) {
auto replace_all = [&]( std::string_view sFind, std::string_view repl ) {
size_t index = 0;
while( ( index = sTarget.find( sFind, index ) ) != sTarget.npos ) {
sTarget.replace( index, sFind.size(), repl );
}
};
replace_all( "struct ", "" ); // msvc
replace_all( "class ", "" ); // msvc
replace_all( "`anonymous-namespace'::", "" ); // msvc
// unify function signatures / formatting for functions
replace_all( "__cdecl ", "" ); // msvc
replace_all( "(void)", "()" ); // msvc
replace_all( " (", "(" ); // clang
{ // clang
std::string_view sFind = "(lambda at ";
size_t index = 0;
while( ( index = sTarget.find( sFind, index ) ) != sTarget.npos ) {
size_t end = sTarget.find(")",index)+1;
sTarget.replace(index, end, "(lambda)" );
}
}
// some library classes
// FIXME: search and replace might break user-defined classes; for example
// mystd::basic_string<char>
replace_all( "std::__cxx11::basic_string<char>", "std::string" ); // gcc
replace_all( "std::basic_string<char,std::char_traits<char>,std::allocator<char> >", "std::string" ); // msvc
replace_all( "std::basic_string<char>", "std::string" ); // clang
return sTarget;
};
const std::array res = [&] LAMBDA_CONSTEVAL {
std::array<char, prettify( std::string( name ) ).size()> data;
[&]( std::string_view str ) { std::copy( str.begin(), str.end(), data.begin() ); }( prettify( std::string( name ) ) );
return data;
}();
return std::string( res.begin(), res.end() );
}
// function that takes type_name, does some modifications, and
// stores it as static array
template <typename T>
CONSTEVAL_STATIC std::string_view modified_type_name() noexcept {
auto modify = []( std::string sTarget ) {
std::string_view sFind = "::";
size_t index = 0;
while( ( index = sTarget.find( sFind, index ) ) != sTarget.npos )
{
sTarget.replace( index, sFind.size(), "." );
++index;
}
return sTarget;
};
static constexpr std::array res = [&] {
std::array<char, modify( type_name<T>() ).size()> data;
[&]( std::string_view str ) { std::copy( str.begin(), str.end(), data.begin() ); }( modify( type_name<T>() ) );
return data;
}();
return std::string_view( res.data(), res.size() );
}
// function that takes type_name, stores it as static array
// and returns string_view
template <typename T>
CONSTEVAL_STATIC std::string_view static_type_name() noexcept {
static constexpr std::array res = [&] {
std::array<char, type_name<T>().size()> data;
[&]( std::string_view str ) { std::copy( str.begin(), str.end(), data.begin() ); }( type_name<T>() );
return data;
}();
return std::string_view( res.data(), res.size() );
}
namespace bar{struct foo{void bar(int, int);};}
using foo = bar::foo;
namespace{struct fwd;};
int main(int, char**) {
auto l = []{};
std::cout <<
"\nnullptr: " << static_type_name<decltype(nullptr)>() <<
"\nbar::foo: " << static_type_name<bar::foo>() <<
"\nstd::string: " << static_type_name<std::string>() <<
"\nlambda: " << static_type_name<decltype(l)>() <<
"\nunnamed ns: " << static_type_name<fwd>() <<
"\nfunction: " << static_type_name<void(*)()>() <<
"\nfunction: " << static_type_name<void()>() <<
"\nfunction: " << static_type_name<void(&)()>() <<
"\nmethod: " << static_type_name<decltype(&foo::bar)>() <<
"\n";
std::cout << "modified: " << modified_type_name<bar::foo>();
} Note to contrary to what I’ve written before, type_name is constexpr. If it would have been consteval, then it would not be as ergonomic, as the returned std::string cannot be used at runtime. With LAMBDA_CONSTEVAL, I tried to ensure that nothing happens at runtime, except for creating an std::string from an array.
Ideally the output of would match for all three compilers, but in practice it might not be possible to achieve. Since my main use-cases are for user-defined class, the current implementation is already good enough.
The implementation can be made for C++17, but the code gets a lot more complicated, even with a CString-like class.
If you have questions, comments, or found typos, the notes are not clear, or there are some errors; then just contact me.