打印类型名称,听起来像是一个很简单的需求,但在目前的C++当中,并非易事。
本文介绍了一些对此需求的分析与实现。1 概述
类型属于type,对象属于value,前者是编译期的东西,后者则是运行期的东西。你可以打印一个变量的值,却无法打印一个类型的名称。那么如何才能实现这个需求?通常来说,解决问题的思路是将新问题转换为已经存在解决方案的旧问题。其一,编译期目前只能输出错误信息,这个错误信息也可以是一种打印类型名称的方法。我们需要做的,就是主动触发报错,可以利用重载决议的相关知识达到这个目的。其二,既然无法直接打印类型,那么就将类型转换为value,从而在运行期进行打印。但是,通过表格暴力转换法其实并不可行,因为类型组合起来实在太多了。此时可以借助一些语言或编译器特性来获取到类型信息,比如通过typid就可以根据类型得到一个简单的名称。思路确定了,接着就可以顺着这个思路设计实现,以下各节展示各种实作法。2 编译期打印类型名称
这种思路是利用错误信息输出类型信息,如何触发错误,如果大家已经读过【洞悉C++函数重载决议】,相信已经有了深刻认识。具体实现如下:
template <typename...> struct type_name {}; template <typename... Ts> struct name_of { using X = typename type_name
error: no type named 'name' in 'struct type_name
template <typename T> void f(T t) { name_of<decltype(t)>(); } int main() { const int i = 1; f(i); }输出为:
error: no type named 'name' in 'struct type_name
3 Demanged Name
另一种方式是借助typeid关键字,通过它可以获得一个std::type_info对象,其结构如下。namespace std { class type_info { public: virtual ~type_info(); bool operator==(const type_info& rhs) const noexcept; bool before(const type_info& rhs) const noexcept; size_t hash_code() const noexcept; const char* name() const noexcept; type_info(const type_info&) = delete; // cannot be copied type_info& operator=(const type_info&) = delete; // cannot be copied }; }其中的成员函数name()就可以返回类型的名称,这样就根据type获取到了value。但是标准说这个名称是基于实现的。
Returns an implementation defined null-terminated character string containing the name of the type. No guarantees are given; in particular, the returned string can be identical for several types and change between invocations of the same program.事实上也的确如此,MSVC返回的是一段可读的类型名称,而gcc, clang返回的是Mangled Name。(Name Mangling内容可以参考【洞悉C++函数重载决议】)
但幸好,它们内部提供的有Demangle API,通过相关API就可以将类型名称转换为可读的名称。这个API定义如下:
namespace abi { extern "C" char* __cxa_demangle (const char* mangled_name, char* buf, size_t* n, int* status); }
这里主要关注第一个参数就可以,其他参数都可以置空。第一个参数就是type_info::name()返回的Mangled Name,返回值为Demangled Name。
因此,现在就可以分而论之,msvc直接使用type_info::name()返回的类型名称就可以;对于gcc/clang,则先使用Demangle API进行解析,次再使用。具体实现如下:#include
4 编译器扩展特性
编译器还存在另一种扩展,包含有类型信息。大家也许用过__func__,这是每个函数内部都会预定义的一个标识符,表示当前函数的名称。于C99添加到C标准,C++11添加到了C++标准,定义如下。static const char __func__[] = "function-name";C++引入的这个说是"implementation-defined string",意思也是基于实现的,不过在三个平台上的输出基本是一致的。这个标识符只包含函数名称,并不会附带模板参数信息。但是与其相关的扩展附带有这部分信息,gcc/clang的扩展为__PRETTY_FUNCTION__,msvc的扩展为__FUNCSIG__。 它们的内容形式也是基于实现的,一个简单的例子如下。
template <typename T> consteval auto type_name() { #ifdef _MSC_VER return __FUNCSIG__; #elif defined(__GNUC__) return __PRETTY_FUNCTION__; #elif defined(__clang__) return __PRETTY_FUNCTION__; #endif } int main() { std::cout << type_name<int>(); }输出分别为:
// gcc consteval auto type_name() [with T = int] // clang auto type_name() [T = int] // msvc auto __cdecl type_name<int>(void)gcc的这种格式不错,clang丢弃了consteval,msvc同样如此,但它加上了函数调用约定。 现在需要做的,就是根据这些信息,解析出想要的信息。可以借助C++17 std::string_view在编译期完成这个工作。具体实现如下。
template <typename T> consteval auto type_name() { std::string_view name, prefix, suffix; #ifdef __clang__ name = __PRETTY_FUNCTION__; prefix = "auto type_name() [T = "; suffix = "]"; #elif defined(__GNUC__) name = __PRETTY_FUNCTION__; prefix = "consteval auto type_name() [with T = "; suffix = "]"; #elif defined(_MSC_VER) name = __FUNCSIG__; prefix = "auto __cdecl type_name<"; suffix = ">(void)"; #endif name.remove_prefix(prefix.size()); name.remove_suffix(suffix.size()); return name; }通过使用std::string_view,以上代码全都发生于编译期。该代码来自https://stackoverflow.com/a/56766138。这个实现方式要比Demanged Name好,不会丢失修饰,类型信息完善,且发生于编译期。缺点也有,编译器扩展一般都是基于实现的,没有标准保证,内容形式可能会改变,依赖于此的实现并不具备较强的稳定性。
5 Circle
对比以上实现,可以发现,反而是第一种办法,即主动触发Name Lookup报错这种方式最简单,且最稳定、最通用。其他方法都依赖了编译器扩展特性,虽然可以达到目的,但技巧偏多,没有保证。大家要是读过之前更新的四章「C++反射」文章,就知道类型名称其实是一个最基本的类型元信息,只要编译器支持反射,那么实现这个需求是再简单不过了。在此,我们就来看看Circle提供的强大元编程能力,是如何优雅地实现这个功能的。注:Circle基本内容,请看C++反射第三章。Circle对于该需求的实现如下:template <typename... Ts> void print_types() { printf("%d - %s ", int..., Ts.string)...; } print_types<int, double, const char*, int&&>(); // output: // 0 - int // 1 - double // 2 - const char* // 3 - int&&是不是太简单了!而且还要强大许多,比如还可以去重、排序:
template <typename... Ts> void f() { printf("unique: "); print_types
6 Static Reflection
本节再说说如何使用C++标准反射来实现该需求,就它目前的发展,还没有Circle的反射强,不过标准反射的「源码注入」能力很强。详情请看C++反射第四章。通过标准元函数name_of()就可以获取类型名称,因此实现其实很简单,代码如下。template <typename T> consteval auto type_name() { return meta::name_of(reflexpr(T)); } int main() { const int i = 1; constexpr auto __dummy = __reflect_print(type_name<decltype(i)>()); }这里,将在编译期输出const int。虽然标准反射目前来说还是一个残缺品,但实现这种需求也比自己实现起来要简单太多了。
7 总结
本文不算太难,串着讲了一些东西,主要是当时研究TAD时写过相关工具,索性写一篇完整的文章。很多时候,编译器推导的类型并不和预期一致,使用本文介绍的工具可以很方便地研究编译器的这些行为。这里还串起了重载决议和反射的相关内容,也算是帮大家回顾一下。
审核编辑:汤梓红
评论
查看更多