For logging and debugging it can be extremely useful to get a string for the name of a type in C++. However, the standard library doesn't supply a great way of doing that. We don't want to use the standard typeid
operator, std::type_info
, or std::type_index
because they use run-time type information (RTTI) (slow!), and tend to return mangled names anyway.
PermalinkFixedView
We'll first define FixedView
, a small class for holding a const
, non-owning view of the type name. This is basically a trimmed-down version of std::span
, intended to be used primarily at compile-time. We could use std::string_view
instead, but we don't want to because it can throw exceptions. It's simple enough to show here in its entirety:
template <typename DataType>
class FixedView
{
public:
//TYPES
using element_type = std::add_const_t<DataType>;
using reference = element_type&;
using pointer = element_type*;
using size_type = std::intmax_t;
//ITERATORS
using iterator = const element_type*;
using reverse_iterator = std::reverse_iterator<iterator>;
//STRUCTORS
constexpr FixedView() = default;
constexpr ~FixedView() = default;
constexpr FixedView(const DataType* aPointer, size_type aSize) :
mBegin(aPointer), mSize(aSize) {}
constexpr FixedView(std::input_iterator auto aBegin, std::input_iterator
auto aEnd) : mBegin(&*aBegin), mSize(aEnd - aBegin) {}
template <size_type Size>
constexpr FixedView(const DataType (&aArray)[Size]) :
mBegin(aArray), mSize(Size - size_type(nIsCharacter<DataType>)) {}
//GET ITERATORS
constexpr iterator begin() const { return mBegin; }
constexpr iterator end() const { return mBegin + mSize; }
constexpr reverse_iterator rbegin() const
{ return std::make_reverse_iterator(end()); }
constexpr reverse_iterator rend() const;
{ return std::make_reverse_iterator(begin()); }
//ACCESS ELEMENTS
constexpr reference operator[](size_type aIndex) const
{ return *(mBegin + aIndex); }
constexpr pointer data() const { return mBegin; }
//SIZE
constexpr size_type size() const { return mSize; }
constexpr bool empty() const { return (size() == 0); }
private:
const element_type* mBegin = nullptr;
size_type mSize = 0;
};
While the definition of nIsCharacter
isn't shown here, it's just a custom type trait, similar to std::is_integral_v
.
PermalinkGet_TypeName()
OK, now how do we get the string? Stackoverflow has a nice solution. First, we'll use the built-in function-name macros to get a string with the type inside of it. Using macros is unfortunate, but I don't know of a better way to do it:
//Adapted from https://stackoverflow.com/a/64490578/576911
template <typename DataType>
consteval FixedView<char> Get_TypeFunctionName()
{
#if defined(__GNUC__) || defined(__clang__)
return FixedView<char>(__PRETTY_FUNCTION__);
#elif defined(_MSC_VER)
return FixedView<char>(__FUNCSIG__);
#else
return FixedView<char>(__func__);
#endif
}
This however generates a string that is different for each compiler. For example:
//GCC: consteval FixedView<char> Get_TypeFunctionName() [with DataType = int]
//MSVC: class FixedView<char> __cdecl Get_TypeFunctionName<int>(void)
std::cout << Get_TypeFunctionName<int>().begin() << "\n";
How do we extract our type name from this mess in a compiler-independent manner? We'd like to avoid hardcoding the lengths of the prefix and suffix around the type name, as those may be different if you have this function defined in a namespace or not.
To do this, we'll first call Get_FunctionName()
for type void
and find the first location of "void"
within it (there is an extra void
in MSVC's!). We'll then know the length of the compiler-specific prefix and suffix strings around the "void," and can then use those to extract our own type name. It's a little messy, but this will do the trick:
//Adapted from https://stackoverflow.com/a/64490578/576911
//Specialize the function for void:
template <>
consteval FixedView<char> Get_TypeName<void>()
{
return FixedView("void");
}
template <typename DataType>
consteval FixedView<char> Get_TypeName()
{
//Find "void" in Get_TypeFunctionName<void>() to get prefix/suffix size
constexpr FixedView sVoidString = Get_TypeFunctionName<void>();
constexpr FixedView sVoidName = Get_TypeName<void>(); //specialized
//Search from the front, as MSVC does (void) at the end :/
constexpr auto sVoidIter = std::search(std::begin(sVoidString),
std::end(sVoidString), std::begin(sVoidName), std::end(sVoidName));
static_assert(sVoidIter != std::end(sVoidString), "Void not found!\n");
//Compute the lengths of prefix and suffix
constexpr auto sPrefixLength = sVoidIter - std::begin(sVoidString);
constexpr auto sSuffixLength = sVoidString.size() - sPrefixLength -
sVoidName.size();
//Now strip the prefix/suffix from the input type string
constexpr FixedView sTypesString = Get_TypeFunctionName<DataType>();
constexpr auto sTypeNameLength = sTypesString.size() - sPrefixLength -
sSuffixLength;
constexpr auto sTypeNameBegin = std::begin(sTypesString) + sPrefixLength;
return FixedView(sTypeNameBegin, sTypeNameLength);
}
Here's a simple example of how to use it:
auto cTypeName = Get_TypeName<const volatile unsigned char*>();
//Prints: const volatile unsigned char*
std::cout << std::string_view(cTypeName.begin(), cTypeName.size()) << "\n";
PermalinkTypeIndex
std::type_index
comes in handy when we want to refer to a specific type in a type-erased way. For example, to indicate the type of a type-erased object, or as a map key for looking up type-specific information or callbacks (both illustrated here). Since std::type_index
relies on RTTI though, we'll create our own TypeIndex
class instead.
How do we uniquely indicate what type is being referenced? Storing a string of the full type name in TypeIndex
would take too much memory. For example, const unsigned long long*
requires 26 bytes (+1 for '\0'
), and user-defined types can be much longer! We could store the FixedView
itself, but that would take 16 bytes and we can get it smaller.
We could hash the type name string into an integer, but what would we do about hash collisions? If our hashing algorithm happens to return the same result for int
and some type in a vendor-supplied library, we would be stuck!
Instead, we will store a function pointer to Get_TypeName<DataType>()
in TypeIndex
. Since the type of a function pointer is solely determined by the arguments and the return type, we can embed our knowledge of the stored type as a function template parameter!
class TypeIndex
{
public:
TypeIndex() = default;
template <typename DataType>
TypeIndex(std::in_place_type_t<DataType>) :
mFunction (Get_TypeName<DataType>) {}
FixedView<char> Get_TypeName() const { return mFunction(); }
friend auto operator<=>(const TypeIndex& aLHS, const TypeIndex& aRHS)
{
return std::bit_cast<std::uintptr_t>(aLHS.mFunction) <=>
std::bit_cast<std::uintptr_t>(aRHS.mFunction);
}
friend bool operator==(const TypeIndex& aLHS, const TypeIndex& aRHS)
{
return std::bit_cast<std::uintptr_t>(aLHS.mFunction) ==
std::bit_cast<std::uintptr_t>(aRHS.mFunction);
}
private:
using GetNameSignature = FixedView<char>();
GetNameSignature* mFunction = Get_TypeName<void>;
};
template <typename DataType>
TypeIndex Make_TypeIndex()
{
return TypeIndex(std::in_place_type<DataType>);
}
The function pointer is stored as the member variable mFunction
. The helper function Make_TypeIndex()
can be used for creating the TypeIndex
, which is easier to use than remembering how to use std::in_place_type
. std::in_place_type
is needed here because we can't directly provide a template argument to a constructor call. Though you could instead pass a template argument using the named constructor idiom.
There are two downsides to using function pointers though: this type cannot be constexpr
, and it cannot be (de)serialized. It can't be constexpr
because function addresses aren't known at compile time. And it can't be (de)serialized because function pointers are unique to the current running process, and will likely be different for different executions of the program.
If we really needed to (de)serialize the TypeIndex
we could instead use strings (large) or hash the type name (collisions). But we probably shouldn't be storing type-erased objects in files anyway: at that point, you probably ought to know what type of information it is that you're saving!
PermalinkConclusion
We can now get the string of a C++ type name, and can use TypeIndex
to uniquely identify a type in a type-erased manner. These improve upon the C++ standard library methods, which utilize RTTI and are thus slow. These utilities come in handy for debugging, and for managing type-erased any objects.