C++打印类型名称的分析与实现

嵌入式技术

1368人已加入

描述

打印类型名称,听起来像是一个很简单的需求,但在目前的C++当中,并非易事。

本文介绍了一些对此需求的分析与实现。

1 概述

类型属于type,对象属于value,前者是编译期的东西,后者则是运行期的东西。你可以打印一个变量的值,却无法打印一个类型的名称。那么如何才能实现这个需求?通常来说,解决问题的思路是将新问题转换为已经存在解决方案的旧问题。其一,编译期目前只能输出错误信息,这个错误信息也可以是一种打印类型名称的方法。我们需要做的,就是主动触发报错,可以利用重载决议的相关知识达到这个目的。其二,既然无法直接打印类型,那么就将类型转换为value,从而在运行期进行打印。但是,通过表格暴力转换法其实并不可行,因为类型组合起来实在太多了。此时可以借助一些语言或编译器特性来获取到类型信息,比如通过typid就可以根据类型得到一个简单的名称。思路确定了,接着就可以顺着这个思路设计实现,以下各节展示各种实作法。

2 编译期打印类型名称 

这种思路是利用错误信息输出类型信息,如何触发错误,如果大家已经读过【洞悉C++函数重载决议】,相信已经有了深刻认识。

具体实现如下:


		template <typename...> struct type_name {}; template <typename... Ts> struct name_of {     using X = typename type_name::name; }; int main() {     name_of<intfloatconst char*>(); }由于Name Lookup查找的名称name_of带有模板,因此会进入重载决议的第二阶段:Template Handling。 模板参数已经显式指出,因此其实并不会进行TAD,而是直接模板参数替换。但是编译器发现type_name::name并没有name类型,因此模板替换失败,产生hard error,编译失败。这个hard error错误信息,就带有类型名称,输出如下:

		
			error: no type named 'name' in 'struct type_name'     7 |     using X = typename type_name::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'       7 |     using X = typename type_name::name;  可以看到,TAD在推导参数时丢弃了top-level const修饰,t实际类型为int。这种形式的优点是完全发生于编译期,实现简单;缺点也很明显,无法指定输出形式,看起来不够直观。

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 charname() 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++函数重载决议】)Type

但幸好,它们内部提供的有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  #include  #include  #include  #ifndef _MSC_VER     #include  #endif // _MSC_VER template <typename T> std::string type_name() {     using type = typename std::remove_reference::type;     // 1. 通过typeid获得类型名称     const char* name = typeid(type).name();     std::string result;     // 2. 通过gcc/clang扩展API获得Demangled Name #ifndef _MSC_VER     char* demangled_name = abi::__cxa_demangle(name, nullptrnullptrnullptr);     result += demangled_name;     free(demangled_name); #else     result += name; #endif // _MSC_VER     // 3. 添加丢弃的修饰     if (std::is_const::value)         result += " const";     if (std::is_volatile::value)      result += " volatile";     if (std::is_lvalue_reference::value) result += "&";     if (std::is_rvalue_reference::value) result += "&&";     return result; } struct Base {}; struct Derived : Base {}; int main() {     std::cout << type_name<const int&>() << " ";     std::cout << type_name() << " ";     std::cout << type_name() << " "; }实现分为三个步骤,注释已经写得很清晰了,这里补充几个重点。第一,Demangled API的返回值是采用malloc()分配的内存,需要手动进行释放。第二,type_info::name()会丢弃cv及引用修饰符,所以还需要手动添加这些修饰。最终输出如下图所示。 Type这种实现方式来自https://stackoverflow.com/a/20170989优点在于,可以统一格式,输出清晰。缺点在于,实现稍微麻烦,要考虑更多情况,且发生于运行期。

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<intdoubleconst char*, int&&>(); // output: // 0 - int // 1 - double // 2 - const char* // 3 - int&&是不是太简单了!而且还要强大许多,比如还可以去重、排序:

		template <typename... Ts> void f() {     printf("unique: ");     print_types();     printf("sort by type name:  ");     print_typesstring < _1.string)...>(); } f<intdoubleconst char*, double,  int&&>(); // output: // unique: // 0 - int // 1 - double // 2 - const char* // 3 - int&& // sort by type name:  // 0 - const char* // 1 - double // 2 - double // 3 - int // 4 - int&&这些操作只要加上@meta就全部可以发生于编译期。 所以说,自己的实现是完全比不上这种编译器自带的类型元信息机制的,因为解决问题时不在一个层次。用户自己实现的永远处在应用层,而编译器则直接在原理层建设。

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时写过相关工具,索性写一篇完整的文章。很多时候,编译器推导的类型并不和预期一致,使用本文介绍的工具可以很方便地研究编译器的这些行为。这里还串起了重载决议和反射的相关内容,也算是帮大家回顾一下。

 

  审核编辑:汤梓红


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分