C++ Get name of type in template

Viewed 167486

I'm writing some template classes for parseing some text data files, and as such it is likly the great majority of parse errors will be due to errors in the data file, which are for the most part not written by programmers, and so need a nice message about why the app failed to load e.g. something like:

Error parsing example.txt. Value ("notaninteger")of [MySectiom]Key is not a valid int

I can work out the file, section and key names from the arguments passed to the template function and member vars in the class, however I'm not sure how to get the name of the type the template function is trying to convert to.

My current code looks like, with specialisations for just plain strings and such:

template<typename T> T GetValue(const std::wstring &section, const std::wstring &key)
{
    std::map<std::wstring, std::wstring>::iterator it = map[section].find(key);
    if(it == map[section].end())
        throw ItemDoesNotExist(file, section, key)
    else
    {
        try{return boost::lexical_cast<T>(it->second);}
        //needs to get the name from T somehow
        catch(...)throw ParseError(file, section, key, it->second, TypeName(T));
    }
}

Id rather not have to make specific overloads for every type that the data files might use, since there are loads of them...

Also I need a solution that does not incur any runtime overhead unless an exception occurs, i.e. a completely compile time solution is what I want since this code is called tons of times and load times are already getting somewhat long.

EDIT: Ok this is the solution I came up with:

I have a types.h containg the following

#pragma once
template<typename T> const wchar_t *GetTypeName();

#define DEFINE_TYPE_NAME(type, name) \
    template<>const wchar_t *GetTypeName<type>(){return name;}

Then I can use the DEFINE_TYPE_NAME macro to in cpp files for each type I need to deal with (eg in the cpp file that defined the type to start with).

The linker is then able to find the appropirate template specialisation as long as it was defined somewhere, or throw a linker error otherwise so that I can add the type.

12 Answers

Jesse Beder's solution is likely the best, but if you don't like the names typeid gives you (I think gcc gives you mangled names for instance), you can do something like:

template<typename T>
struct TypeParseTraits;

#define REGISTER_PARSE_TYPE(X) template <> struct TypeParseTraits<X> \
    { static const char* name; } ; const char* TypeParseTraits<X>::name = #X


REGISTER_PARSE_TYPE(int);
REGISTER_PARSE_TYPE(double);
REGISTER_PARSE_TYPE(FooClass);
// etc...

And then use it like

throw ParseError(TypeParseTraits<T>::name);

EDIT:

You could also combine the two, change name to be a function that by default calls typeid(T).name() and then only specialize for those cases where that's not acceptable.

This trick was mentioned under a few other questions, but not here yet.

All major compilers support __PRETTY_FUNC__ (GCC & Clang) /__FUNCSIG__ (MSVC) as an extension.

When used in a template like this:

template <typename T> const char *foo()
{
    #ifdef _MSC_VER
    return __FUNCSIG__;
    #else
    return __PRETTY_FUNCTION__;
    #endif
}

It produces strings in a compiler-dependent format, that contain, among other things, the name of T.

E.g. foo<float>() returns:

  • "const char* foo() [with T = float]" on GCC
  • "const char *foo() [T = float]" on Clang
  • "const char *__cdecl foo<float>(void)" on MSVC

You can easily parse the type names out of those strings. You just need to figure out how many 'junk' characters your compiler inserts before and after the type.

You can even do that completely at compile-time.


The resulting names can slightly vary between different compilers. E.g. GCC omits default template arguments, and MSVC prefixes classes with the word class.


Here's an implementation that I've been using. Everything is done at compile-time.

Example usage:

std::cout << TypeName<float>() << '\n';
std::cout << TypeName<decltype(1.2f)>(); << '\n';

Implementation: (uses C++20, but can be backported; see the edit history for a C++17 version)

#include <algorithm>
#include <array>
#include <cstddef>
#include <string_view>

namespace impl
{
    template <typename T>
    [[nodiscard]] constexpr std::string_view RawTypeName()
    {
        #ifndef _MSC_VER
        return __PRETTY_FUNCTION__;
        #else
        return __FUNCSIG__;
        #endif
    }

    struct TypeNameFormat
    {
        std::size_t junk_leading = 0;
        std::size_t junk_total = 0;
    };

    constexpr TypeNameFormat type_name_format = []{
        TypeNameFormat ret;
        std::string_view sample = RawTypeName<int>();
        ret.junk_leading = sample.find("int");
        ret.junk_total = sample.size() - 3;
        return ret;
    }();
    static_assert(type_name_format.junk_leading != std::size_t(-1), "Unable to determine the type name format on this compiler.");

    template <typename T>
    static constexpr auto type_name_storage = []{
        std::array<char, RawTypeName<T>().size() - type_name_format.junk_total + 1> ret{};
        std::copy_n(RawTypeName<T>().data() + type_name_format.junk_leading, ret.size() - 1, ret.data());
        return ret;
    }();
}

template <typename T>
[[nodiscard]] constexpr std::string_view TypeName()
{
    return {impl::type_name_storage<T>.data(), impl::type_name_storage<T>.size() - 1};
}

template <typename T>
[[nodiscard]] constexpr const char *TypeNameCstr()
{
    return impl::type_name_storage<T>.data();
}

If you'd like a pretty_name, Logan Capaldo's solution can't deal with complex data structure: REGISTER_PARSE_TYPE(map<int,int>) and typeid(map<int,int>).name() gives me a result of St3mapIiiSt4lessIiESaISt4pairIKiiEEE

There is another interesting answer using unordered_map or map comes from https://en.cppreference.com/w/cpp/types/type_index.

#include <iostream>
#include <unordered_map>
#include <map>
#include <typeindex>
using namespace std;
unordered_map<type_index,string> types_map_;

int main(){
    types_map_[typeid(int)]="int";
    types_map_[typeid(float)]="float";
    types_map_[typeid(map<int,int>)]="map<int,int>";

    map<int,int> mp;
    cout<<types_map_[typeid(map<int,int>)]<<endl;
    cout<<types_map_[typeid(mp)]<<endl;
    return 0;
}

typeid(uint8_t).name() is nice, but it returns "unsigned char" while you may expect "uint8_t".

This piece of code will return you the appropriate type

#define DECLARE_SET_FORMAT_FOR(type) \
    if ( typeid(type) == typeid(T) ) \
        formatStr = #type;

template<typename T>
static std::string GetFormatName()
{
    std::string formatStr;

    DECLARE_SET_FORMAT_FOR( uint8_t ) 
    DECLARE_SET_FORMAT_FOR( int8_t ) 

    DECLARE_SET_FORMAT_FOR( uint16_t )
    DECLARE_SET_FORMAT_FOR( int16_t )

    DECLARE_SET_FORMAT_FOR( uint32_t )
    DECLARE_SET_FORMAT_FOR( int32_t )

    DECLARE_SET_FORMAT_FOR( float )

    // .. to be exptended with other standard types you want to be displayed smartly

    if ( formatStr.empty() )
    {
        assert( false );
        formatStr = typeid(T).name();
    }

    return formatStr;
}

I just leave it there. If someone will still need it, then you can use this:

template <class T>
bool isString(T* t) { return false;  } // normal case returns false

template <>
bool isString(char* t) { return true; }  // but for char* or String.c_str() returns true
.
.
.

This will only CHECK type not GET it and only for 1 type or 2.

There're many good answers here, but I'd guess the easiest is to use a library, isn't it? You can use this tiny lib, it's pretty cross-platform (tested with recent GCC/Clang/MSVC/ICC), supports C++11 onwards, works at compile time (consteval since C++20) and in theory it's going to support every compiler since the C++20's <source_location> gains wider support. A few more notes are in the README :)

I found this trick to publish a service interface with type information

#include <iostream>

using namespace std;
const char* nameOfType(int& ){ return "int";}
const char* nameOfType(const char* &){ return "string";}
const char* nameOfType(float& ){ return "float";}
const char* nameOfType(double& ){ return "double";}

template <typename T> 
const char* templateFunction(T t){
  return nameOfType(t)  ;
};

int main()
{
    cout<<"Hello World this is an ";
    cout << templateFunction<int>(1) << std::endl;
    cout << templateFunction<const char*>("") << std::endl;
    cout << templateFunction<double>(3.14) <<std::endl;
    return 0;
}

Hope it can help somebody.

Related