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

打印类型名称,听起来像是一个很简单的需求,但在目前的c++当中,并非易事。
本文介绍了一些对此需求的分析与实现。 1 概述 类型属于type,对象属于value,前者是编译期的东西,后者则是运行期的东西。你可以打印一个变量的值,却无法打印一个类型的名称。那么如何才能实现这个需求?通常来说,解决问题的思路是将新问题转换为已经存在解决方案的旧问题。其一,编译期目前只能输出错误信息,这个错误信息也可以是一种打印类型名称的方法。我们需要做的,就是主动触发报错,可以利用重载决议的相关知识达到这个目的。其二,既然无法直接打印类型,那么就将类型转换为value,从而在运行期进行打印。但是,通过表格暴力转换法其实并不可行,因为类型组合起来实在太多了。此时可以借助一些语言或编译器特性来获取到类型信息,比如通过typid就可以根据类型得到一个简单的名称。思路确定了,接着就可以顺着这个思路设计实现,以下各节展示各种实作法。 2 编译期打印类型名称  这种思路是利用错误信息输出类型信息,如何触发错误,如果大家已经读过【洞悉c++函数重载决议】,相信已经有了深刻认识。 具体实现如下:
template  struct type_name {}; template  struct name_of {     using x = typename type_name::name; }; int main() {     name_of(); }由于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  void f(t t) {     name_of(); } 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 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  #include  #include  #include  #ifndef _msc_ver     #include  #endif // _msc_ver template  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, nullptr, nullptr, nullptr);     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() <<  ;     std::cout << type_name() <<  ;     std::cout << type_name() <<  ; }实现分为三个步骤,注释已经写得很清晰了,这里补充几个重点。第一,demangled api的返回值是采用malloc()分配的内存,需要手动进行释放。第二,type_info::name()会丢弃cv及引用修饰符,所以还需要手动添加这些修饰。最终输出如下图所示。 这种实现方式来自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  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(); }输出分别为: // gcc consteval auto type_name() [with t = int] // clang auto type_name() [t = int] // msvc auto __cdecl type_name(void)gcc的这种格式不错,clang丢弃了consteval,msvc同样如此,但它加上了函数调用约定。 现在需要做的,就是根据这些信息,解析出想要的信息。可以借助c++17 std::string_view在编译期完成这个工作。具体实现如下。 template  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(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  void print_types() {     printf(%d - %s , int..., ts.string)...; } print_types(); // output: // 0 - int // 1 - double // 2 - const char* // 3 - int&&是不是太简单了!而且还要强大许多,比如还可以去重、排序: template  void f() {     printf(unique: );     print_types();     printf(sort by type name:  );     print_types

微服务浪潮中,程序猿如何让自己 Be Cloud Native
Socket AM2是什么?
什么是盐雾测试?不锈钢能耐多长时间盐雾测试?
GOSSEN摄影师的理想测光工具:高品质测光表DIGISKY
新型高精度技术让纳米图形“变身”二维材料
C++打印类型名称的分析与实现
直布罗陀区块链交易所CEO:区块链创新的阻碍来自于欧盟
诺基亚8亮相,率先搭载自家OZO音频技术
OPPOR15和vivoX21买哪个好
服务提供商如何降低其5G基础设施的成本
基于DSP与FPGA的实时功率谱分析系统设计
关于MATLAB/Simulink技术研讨会—智能自主系统开发分析和介绍
长城最新发布的这款WEY-Pi4-VV7x霸气十足,跨界范不输宝马X6,预售价18万起
华为Mate30系列设计背后的故事
如何实现可穿戴设计的蓝牙连接
荣耀9评测:千机一变,友商们又可以出新颜色啦!胡歌同款奶奶灰,你值得拥有!
一文知道MCU上电复位启动过程
自粘结铁芯对电机能效提升到底有多大影响?
贸泽电子携手Microchip推出全新电子书 探索新一代物联网各类出色应用
扬声器和智能放大器:什么使放大器变得智能?