打印類型名稱,聽(tīng)起來(lái)像是一個(gè)很簡(jiǎn)單的需求,但在目前的C++當(dāng)中,并非易事。
本文介紹了一些對(duì)此需求的分析與實(shí)現(xiàn)。1?概述
類型屬于type,對(duì)象屬于value,前者是編譯期的東西,后者則是運(yùn)行期的東西。你可以打印一個(gè)變量的值,卻無(wú)法打印一個(gè)類型的名稱。那么如何才能實(shí)現(xiàn)這個(gè)需求?通常來(lái)說(shuō),解決問(wèn)題的思路是將新問(wèn)題轉(zhuǎn)換為已經(jīng)存在解決方案的舊問(wèn)題。其一,編譯期目前只能輸出錯(cuò)誤信息,這個(gè)錯(cuò)誤信息也可以是一種打印類型名稱的方法。我們需要做的,就是主動(dòng)觸發(fā)報(bào)錯(cuò),可以利用重載決議的相關(guān)知識(shí)達(dá)到這個(gè)目的。其二,既然無(wú)法直接打印類型,那么就將類型轉(zhuǎn)換為value,從而在運(yùn)行期進(jìn)行打印。但是,通過(guò)表格暴力轉(zhuǎn)換法其實(shí)并不可行,因?yàn)轭愋徒M合起來(lái)實(shí)在太多了。此時(shí)可以借助一些語(yǔ)言或編譯器特性來(lái)獲取到類型信息,比如通過(guò)typid就可以根據(jù)類型得到一個(gè)簡(jiǎn)單的名稱。思路確定了,接著就可以順著這個(gè)思路設(shè)計(jì)實(shí)現(xiàn),以下各節(jié)展示各種實(shí)作法。2?編譯期打印類型名稱?
這種思路是利用錯(cuò)誤信息輸出類型信息,如何觸發(fā)錯(cuò)誤,如果大家已經(jīng)讀過(guò)【洞悉C++函數(shù)重載決議】,相信已經(jīng)有了深刻認(rèn)識(shí)。具體實(shí)現(xiàn)如下:
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關(guān)鍵字,通過(guò)它可以獲得一個(gè)std::type_info對(duì)象,其結(jié)構(gòu)如下。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 ????}; }其中的成員函數(shù)name()就可以返回類型的名稱,這樣就根據(jù)type獲取到了value。但是標(biāo)準(zhǔn)說(shuō)這個(gè)名稱是基于實(shí)現(xiàn)的。
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.事實(shí)上也的確如此,MSVC返回的是一段可讀的類型名稱,而gcc, clang返回的是Mangled Name。(Name Mangling內(nèi)容可以參考【洞悉C++函數(shù)重載決議】)
但幸好,它們內(nèi)部提供的有Demangle API,通過(guò)相關(guān)API就可以將類型名稱轉(zhuǎn)換為可讀的名稱。這個(gè)API定義如下:
namespace?abi?{ ??extern?"C"?char*?__cxa_demangle?(const?char*?mangled_name, ???????????????????char*?buf, ???????????????????size_t*?n, ???????????????????int*?status); }
這里主要關(guān)注第一個(gè)參數(shù)就可以,其他參數(shù)都可以置空。第一個(gè)參數(shù)就是type_info::name()返回的Mangled Name,返回值為Demangled Name。
因此,現(xiàn)在就可以分而論之,msvc直接使用type_info::name()返回的類型名稱就可以;對(duì)于gcc/clang,則先使用Demangle API進(jìn)行解析,次再使用。具體實(shí)現(xiàn)如下:#include?
4?編譯器擴(kuò)展特性
編譯器還存在另一種擴(kuò)展,包含有類型信息。大家也許用過(guò)__func__,這是每個(gè)函數(shù)內(nèi)部都會(huì)預(yù)定義的一個(gè)標(biāo)識(shí)符,表示當(dāng)前函數(shù)的名稱。于C99添加到C標(biāo)準(zhǔn),C++11添加到了C++標(biāo)準(zhǔn),定義如下。static?const?char?__func__[]?=?"function-name";C++引入的這個(gè)說(shuō)是"implementation-defined string",意思也是基于實(shí)現(xiàn)的,不過(guò)在三個(gè)平臺(tái)上的輸出基本是一致的。這個(gè)標(biāo)識(shí)符只包含函數(shù)名稱,并不會(huì)附帶模板參數(shù)信息。但是與其相關(guān)的擴(kuò)展附帶有這部分信息,gcc/clang的擴(kuò)展為__PRETTY_FUNCTION__,msvc的擴(kuò)展為__FUNCSIG__。 它們的內(nèi)容形式也是基于實(shí)現(xiàn)的,一個(gè)簡(jiǎn)單的例子如下。
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的這種格式不錯(cuò),clang丟棄了consteval,msvc同樣如此,但它加上了函數(shù)調(diào)用約定。 現(xiàn)在需要做的,就是根據(jù)這些信息,解析出想要的信息。可以借助C++17 std::string_view在編譯期完成這個(gè)工作。具體實(shí)現(xiàn)如下。
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; }通過(guò)使用std::string_view,以上代碼全都發(fā)生于編譯期。該代碼來(lái)自https://stackoverflow.com/a/56766138。這個(gè)實(shí)現(xiàn)方式要比Demanged Name好,不會(huì)丟失修飾,類型信息完善,且發(fā)生于編譯期。缺點(diǎn)也有,編譯器擴(kuò)展一般都是基于實(shí)現(xiàn)的,沒(méi)有標(biāo)準(zhǔn)保證,內(nèi)容形式可能會(huì)改變,依賴于此的實(shí)現(xiàn)并不具備較強(qiáng)的穩(wěn)定性。
5?Circle
對(duì)比以上實(shí)現(xiàn),可以發(fā)現(xiàn),反而是第一種辦法,即主動(dòng)觸發(fā)Name Lookup報(bào)錯(cuò)這種方式最簡(jiǎn)單,且最穩(wěn)定、最通用。其他方法都依賴了編譯器擴(kuò)展特性,雖然可以達(dá)到目的,但技巧偏多,沒(méi)有保證。大家要是讀過(guò)之前更新的四章「C++反射」文章,就知道類型名稱其實(shí)是一個(gè)最基本的類型元信息,只要編譯器支持反射,那么實(shí)現(xiàn)這個(gè)需求是再簡(jiǎn)單不過(guò)了。在此,我們就來(lái)看看Circle提供的強(qiáng)大元編程能力,是如何優(yōu)雅地實(shí)現(xiàn)這個(gè)功能的。注:Circle基本內(nèi)容,請(qǐng)看C++反射第三章。Circle對(duì)于該需求的實(shí)現(xiàn)如下: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&&是不是太簡(jiǎn)單了!而且還要強(qiáng)大許多,比如還可以去重、排序:
template?<typename...?Ts> void?f()?{ ????printf("unique: "); ????print_types
6?Static Reflection
本節(jié)再說(shuō)說(shuō)如何使用C++標(biāo)準(zhǔn)反射來(lái)實(shí)現(xiàn)該需求,就它目前的發(fā)展,還沒(méi)有Circle的反射強(qiáng),不過(guò)標(biāo)準(zhǔn)反射的「源碼注入」能力很強(qiáng)。詳情請(qǐng)看C++反射第四章。通過(guò)標(biāo)準(zhǔn)元函數(shù)name_of()就可以獲取類型名稱,因此實(shí)現(xiàn)其實(shí)很簡(jiǎn)單,代碼如下。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。雖然標(biāo)準(zhǔn)反射目前來(lái)說(shuō)還是一個(gè)殘缺品,但實(shí)現(xiàn)這種需求也比自己實(shí)現(xiàn)起來(lái)要簡(jiǎn)單太多了。
7?總結(jié)
本文不算太難,串著講了一些東西,主要是當(dāng)時(shí)研究TAD時(shí)寫(xiě)過(guò)相關(guān)工具,索性寫(xiě)一篇完整的文章。很多時(shí)候,編譯器推導(dǎo)的類型并不和預(yù)期一致,使用本文介紹的工具可以很方便地研究編譯器的這些行為。這里還串起了重載決議和反射的相關(guān)內(nèi)容,也算是幫大家回顧一下。?
審核編輯:湯梓紅
評(píng)論
查看更多