C++ 获取模板中类型的名称

115
我正在编写一些模板类来解析一些文本数据文件,因此很可能大部分解析错误都是由于数据文件中的错误导致的,这些错误大多数情况下不是由程序员编写的,因此需要一个关于应用程序加载失败原因的好消息,例如:

解析 example.txt 错误。[MySectiom]Key 的值 ("notaninteger") 不是有效的整数

我可以从模板函数和类的成员变量传递的参数中找出文件、节和键名,但我不知道如何获取模板函数试图转换为的类型的名称。

我的当前代码看起来像这样,只是针对普通字符串等进行了特化:

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

我不想为数据文件可能使用的每种类型创建特定的重载,因为有很多...
此外,我需要一个解决方案,除非出现异常,否则不会产生任何运行时开销,即我想要一个完全的编译时解决方案,因为这段代码被调用了很多次,加载时间已经变得有些长了。
编辑:好的,这是我想出的解决方案:
我有一个types.h,其中包含以下内容。
#pragma once
template<typename T> const wchar_t *GetTypeName();

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

然后我可以在每个需要处理的类型的cpp文件中使用DEFINE_TYPE_NAME宏(例如,在定义要处理的类型的cpp文件中)。

只要模板特化已经被定义,链接器就能够找到正确的特化版本;如果没有定义,则会抛出链接器错误,以便我可以添加该类型。


1
并不是与你的问题相关,但当访问该部分时,你可能还想使用map.find(section),除非你有意创建一个空部分。 - Idan K
14个回答

113

解决方案是:

typeid(T).name()

typeid(T) 返回 std::type_info


8
请记住,返回每种类型相同的字符串是符合规范的(尽管我不认为任何编译器会这样做)。 - Motti
3
或者在不同的执行中为相同类型返回不同的字符串(尽管我认为任何理智的编译器都不会这样做)。 - Emily L.
7
我想指出给定名称可能很难看:“typeid(simd::double3x4).name() = "N4simd9double3x4E"”。“typeid(simd::float4).name() = "Dv4_f"”。这是 C++17,Xcode 10.1。 - Andreas detests censorship
1
确实。typeid(T).name()是这样做的规范方式,但很少有编译器返回未混淆的名称;我个人熟悉的唯一一个能够这样做的是MSVC。根据使用的编译器,还有可能会丢失一些函数类型的类型信息,但在这种情况下这可能是不相关的。 - Justin Time - Reinstate Monica
3
typeid(T).name() 并不返回 std::type_info,而是返回 char const * 类型的字符串。 - lmat - Reinstate Monica

64

typeid(T).name()的结果是由实现定义的,并不能保证返回的字符串易于读懂。

cppreference.com上可以看到:

返回一个以空字符结尾的字符串,包含类型名称,其内容由实现定义。没有任何保证,特别是,返回的字符串对于多个类型可能相同,在同一程序调用中可能发生更改。

...

使用像gcc和clang这样的编译器,返回的字符串可以通过c++filt -t进行转换以获得易于读懂的形式。

但在某些情况下,gcc会返回错误的字符串。例如,在我机器上为带有-std=c++11选项的gcc中,模板函数typeid(T).name()返回"j",而不是"unsigned int"。这被称为名称混淆。要获取真实类型名称,请使用abi::__cxa_demangle()函数(仅适用于gcc):

#include <string>
#include <cstdlib>
#include <cxxabi.h>

template<typename T>
std::string type_name()
{
    int status;
    std::string tname = typeid(T).name();
    char *demangled_name = abi::__cxa_demangle(tname.c_str(), NULL, NULL, &status);
    if(status == 0) {
        tname = demangled_name;
        std::free(demangled_name);
    }   
    return tname;
}

1
if语句中使用free会不会导致内存泄漏? - Tomáš Zato
2
不行,因为如果状态不为0,则指针指向nullptr - Henry Schreiner
3
我想补充一下,在编写代码时最好检查是否存在gcc或clang,如果不存在则默认不进行名称还原(可以参考这里的代码:http://ideone.com/sqFWir)。 - god of llamas
1
这个解决方案没有显示类型限定符(例如const),这里有一个类似的方法可以同时显示类型和限定符 - arkan

47

Jesse Beder的解决方案可能是最好的,但如果你不喜欢typeid给你的名称(例如,我认为gcc会给你一些混淆的名称),你可以做如下操作:

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...

然后像这样使用它

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

编辑:

您还可以将两者结合起来,将name更改为默认调用typeid(T).name()的函数,然后仅针对那些不可接受的情况进行特化。


注意:如果您忘记为使用的类型定义REGISTER_PARSE_TYPE,则此代码将无法编译。我以前在没有RTTI的代码中使用过类似的技巧,效果非常好。 - Tom Leys
1
我不得不将名称移出结构体,因为在g++ 4.3.0中有一个错误:"error: invalid in-class initialization of static data member of non-integral type 'const char *'";当然,在TypeParseTraits的<>之间需要关键字“struct”,并且定义应该以分号结束。 - fuzzyTew
4
留下分号是故意的,它强制你在宏调用结束时使用它,但感谢你的更正。 - Logan Capaldo
我得到了以下错误:错误:'#'后面没有宏参数 - kratsg
@kratsg - 这是因为最后应该是 '#X' 而不是 '#x'(大写以匹配宏参数)- 我会修复这个答案。 - amdn

27

正如Bunkar所提到的,typeid(T).name是实现定义的。

为了避免这个问题,您可以使用Boost.TypeIndex库。

例如:

boost::typeindex::type_id<T>().pretty_name() // human readable

这对于在函数调用时查找模板类型名称非常有用。它对我来说效果很不错。 - Fernando
1
请注意,pretty_name()或raw_name()仍然是实现定义的。在MSVC上,对于一个结构体A;你会得到:"struct A",而在gcc/clang上则是:"A"。 - daminetreg
哇,boost 再次获胜。令人惊奇的是,在编译器不支持 (auto, regex, foreach, threads, static_assert 等等...) 之前,boost 能够做到这些。 - Trevor Boyd Smith

25

在其他几个问题中提到过这个技巧,但在这里尚未提到。

所有主要的编译器都支持 __PRETTY_FUNC__ (GCC & Clang) /__FUNCSIG__ (MSVC) 作为扩展功能。

当在模板中使用时,可以像这样:

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

它以编译器相关格式生成字符串,其中包含了T的名称等信息。

例如,foo<float>()返回:

  • 在GCC上,返回"const char* foo() [with T = float]"
  • 在Clang上,返回"const char *foo() [T = float]"
  • 在MSVC上,返回"const char *__cdecl foo<float>(void)"

您可以轻松解析这些字符串中的类型名称。您只需要找出编译器在类型之前和之后插入了多少“垃圾”字符。

您甚至可以完全在编译时完成。


不同编译器生成的名称可能略有不同。例如,GCC省略默认模板参数,而MSVC在类名前加上单词class


以下是我一直在使用的实现。所有操作都在编译时完成。

示例用法:

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

实现:(使用C++20,但可以回溯;请参见编辑历史记录以获取C++17版本)

#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();
}

那才是真正的答案!!绝对漂亮,不需要标准库,并且它在编译时运行。在嵌入式代码中,这是唯一的解决方案。谢谢! - György Gulyás
我也喜欢这个答案,但要注意 GCC < 8.0 在默认的 Ubuntu 18 上不将 __PRETTY_FUNCTION__ 定义为 constexpr。此外,这里有一个类似概念的实现(也基于 __PRETTY_FUNCTION__)。 - eclarkso
这对我有用。我直接打印了__PRETTY_FUNCTION__的字符串输出。我的代码库不支持typeid。 - shivank

14

Logan Capaldo的回答是正确的,但可以稍微简化,因为并不需要每次都专门化类。可以这样写:

Logan Capaldo的答案正确,但可以稍作简化,因为每次都特化类并不必要。我们可以这样编写代码:

// in header
template<typename T>
struct TypeParseTraits
{ static const char* name; };

// in c-file
#define REGISTER_PARSE_TYPE(X) \
    template <> const char* TypeParseTraits<X>::name = #X

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

这还允许您将REGISTER_PARSE_TYPE指令放入C++文件中...


8
作为 Andrey 回答的重述: Boost TypeIndex 库可用于打印类型名称。
在模板中,可能会这样写:
#include <boost/type_index.hpp>
#include <iostream>

template<typename T>
void printNameOfType() {
    std::cout << "Type of T: " 
              << boost::typeindex::type_id<T>().pretty_name() 
              << std::endl;
}

2
如果你想要一个漂亮的名称,Logan Capaldo的解决方案无法处理复杂的数据结构:REGISTER_PARSE_TYPE(map<int,int>)typeid(map<int,int>).name()给出了一个结果St3mapIiiSt4lessIiESaISt4pairIKiiEEE 还有另一个有趣的答案来自于使用unordered_mapmap,参考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;
}

2

typeid(uint8_t).name() 很好,但它返回的是 "unsigned char",而你可能期望的是 "uint8_t"。

以下代码将会返回适当的类型:

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

这很好,但为什么不用 return #type; 呢? - Little Helper
@LittleHelper:你说得对,那也可以行得通... - jpo38

1
自C++20起,我们可以使用std::source_location::function_name()来获取一个包含函数名称和参数的字符串。
template<typename T>
consteval auto type_name()
{
    std::string_view func_name(std::source_location::current().function_name()); // returns something like: consteval auto type_name() [with T = int]

    auto extracted_params = ... Do some post processing here to extract the parameter names.
    return extracted_params;
}

注意:截至2022年10月,MSVC不会报告模板参数,因此该解决方案在那里无法使用。不幸的是,function_name()的返回值形式在标准中没有指定,但我们至少可以希望他们在以后的版本中添加模板参数。

示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接