Getting Type Names in C++

Getting Type Names in C++

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.

FixedView

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;
};

Get_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";

TypeIndex

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!

Conclusion

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.

License & Disclaimers

Support This Blog!

Did you find this article valuable?

Support Paul Mattione by becoming a sponsor. Any amount is appreciated!