打印類型名稱,聽起來像是一個很簡單的需求,但在目前的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?<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時寫過相關工具,索性寫一篇完整的文章。很多時候,編譯器推導的類型并不和預期一致,使用本文介紹的工具可以很方便地研究編譯器的這些行為。這里還串起了重載決議和反射的相關內容,也算是幫大家回顧一下。?
審核編輯:湯梓紅
評論
查看更多