天天看點

如何在C++中獲得完整的類型名稱一、如何檢查C++中的類型二、儲存和輸出字元串三、輸出有效的類型定義尾聲

地球人都知道C++裡有一個typeid操作符可以用來擷取一個類型/表達式的名稱:

但是這個name()的傳回值是,在vc和gcc中列印出來的結果如下:

一個稍微長一點的類型名稱,比如:

打出來是這個效果:

(話說gcc您的傳回結果真是。。)

當然了,想在gcc裡得到和微軟差不多顯示效果的方法也是有的,那就是使用:

顯示效果:

先不說不同編譯器下的适配問題,來看看下面這個會列印出啥:

可愛的cv限定符和引用都被丢掉了=.=

如果直接在typeid的結果上加上被丢棄的資訊,對于一些類型而言(如函數指針引用)得到的将不是一個正确的類型名稱。

想要獲得一個類型的完整名稱,并且獲得的名稱必須要是一個正确的類型名稱,應該怎樣做呢?

我們需要一個泛型類,用特化/偏特化機制靜态檢查出C++中的各種類型,并且不能忽略掉類型限定符(type-specifiers)和各種聲明符(declarators)。

先來考慮一個最簡單的類模闆:

假如在它的基礎上特化,需要寫多少個版本呢?我們可以稍微實作下試試:

這還遠遠沒有完。有同學可能會說了,我們不是有偉大的宏嘛,這些東西都像是一個模子刻出來的,弄一個宏批量生成下不就完了。

實際上當我們真的信心滿滿的動手去寫這些宏的時候,才發現适配上的細微差别會讓宏寫得非常痛苦(比如&和*的差别,[]和[N]的差别,還有函數類型、函數指針、函數指針引用、函數指針數組、類成員指針、……)。當我們一一羅列出需要特化的細節時,不由得感歎C++類型系統的複雜和糾結。

但是上面的理由并不是這個思路的緻命傷。

不可行的地方在于:我們可以寫一個多元指針,或多元數組,類型是可以嵌套的。總不可能為每一個次元都特化一個模闆吧。

不過正由于類型其實是嵌套的,我們可以用模闆元程式設計的基本思路來搞定這個問題:

一個簡單的繼承,就讓特化變得simple很多。因為當我們萃取出一個類型,比如T *,之後的T其實是攜帶上了除*之外所有其他類型資訊的一個類型。那麼把這個T再重複投入check中,就會繼續萃取它的下一個類型特征。

可以先用指針、引用的萃取來看看效果:

輸出結果(vc):

很漂亮,是不是?當然,在gcc裡這樣輸出,void會變成v,是以gcc下面要這樣寫check模闆:

我們可以簡單的這樣修改check讓它同時支援vc和gcc:

但是到目前為止,check的輸出結果都是無法儲存的。比較好的方式是可以像typeid(T).name()一樣傳回一個字元串。這就要求check能夠把結果儲存在一個std::string對象裡。

當然了,我們可以直接給check一個“std::string& out”類型的構造函數,但是這樣會把輸出的狀态管理、字元的列印邏輯等等都揉在一起。是以,比較好的設計方法是實作一個output類,負責輸出和維護狀态。我們到後面就會慢慢感覺到這樣做的好處在哪裡。

output類的實作可以是這樣:

這個小巧的output類負責自動管理輸出狀态(是否增加空格)和輸出的類型轉換(使用std::ostringstream)。

上面的實作裡有兩個比較有意思的地方。

一是operator()的做法,采用了變參模闆。這種做法讓我們可以這樣用output:

這種寫法比cout的流操作符舒服多了。

二是operator()和compact的傳回值。當然,這裡可以直接使用void,但是這會造成一些限制。

比如說,我們想在使用operator()之後馬上compact呢?若讓函數傳回自身對象的引用,就可以讓output用起來非常順手:

check的定義和CHECK_TYPE__宏隻需要略作修改就可以使用output類:

為了讓外部的使用依舊簡潔,實作一個外敷函數模闆是很自然的事情:

如果我們想實作表達式的類型輸出,使用decltype包裹一下就行了。

不知道看到這裡的朋友有沒有注意到,check在gcc下的輸出可能會出現問題。原因是abi::__cxa_demangle并不能保證永遠傳回一個有效的字元串。

我們來看看這個函數的:

“Returns: A pointer to the start of the NUL-terminated demangled name, or NULL if the demangling fails. The caller is responsible for deallocating this memory using free.”

是以說比較好的做法應該是在abi::__cxa_demangle傳回空的時候,直接使用typeid(T).name()的結果。

一種健壯的寫法可以像這樣:

上面我們通過使用std::unique_ptr配合lambda的自定義deleter,實作了一個簡單的,來保證當abi::__cxa_demangle傳回的非NULL指針一定會被free掉。

上面的特化解決了cv限定符、引用和指針,甚至對于未特化的數組、類成員指針等都有還不錯的顯示效果,不過卻無法保證輸出的類型名稱一定是一個有效的類型定義。比如說:

原因是因為這個類型是一個指針,指向一個int[],是以會先比對到指針的特化,是以*就被寫到了最後面。

對于數組、函數等類型來說,若它們處在一個複合類型(compound types)中“子類型”的位置上,它們就需要用括号把它們的“父類型”給括起來。

是以我們還需要預先完成下面這些工作:

1. 如何判斷數組、函數等類型的特化處于check繼承鍊中“被繼承”(也就是某個類的基類)的位置上

2. 圓括号()、方括号[],以及函數參數清單的輸出邏輯

上面的第1點,可以利用模闆偏特化這種靜态的判斷來解決。比如說,給check添加一個預設的bool模闆參數:

這個小小的修改就可以讓check在繼承的時候把父-子資訊傳遞下去。

接下來先考慮圓括号的輸出邏輯。我們可以建構一個bracket類,在編譯期幫我們自動處理圓括号:

在bracket裡,不僅實作了圓括号的輸出,其實還實作了一個編譯期if的小功能。當不輸出圓括号時,我們可以給bracket指定一個其它的輸出内容。

當然,不實作bracket,直接在check的類型特化裡處理括号邏輯也可以,但是這樣的話邏輯就被某個check特化綁死了。我們可以看到bracket的邏輯被剝離出來以後,後面所有需要輸出圓括号的部分都可以直接複用這個功能。

然後是[]的輸出邏輯。考慮到對于[N]類型的數組,還需要把N的具體數值輸出來,是以輸出邏輯可以這樣寫:

輸出邏輯需要寫在bound類的析構,而不是構造裡。原因是對于一個數組類型,[N]總是寫在最後面的。

這裡在輸出的時候直接使用了運作時的if-else,而沒有再用特化來處理。是因為當N是一個編譯期數值時,對于現代的編譯器來說“if (N == 0) ; else ;”語句會被優化掉,隻生成确定邏輯的彙編碼。

最後,是函數參數的輸出邏輯。函數參數清單需要使用變參模闆适配,用編譯期遞歸的元程式設計手法輸出參數,最後在兩頭加上括号。

我們可以先寫出遞歸的結束條件:

輸出邏輯寫在析構裡的理由,和bound一緻。結束條件是顯然的:當參數包為空時,parameter将隻輸出一對括号。

注意到模闆的bool類型參數,讓我們在使用的時候需要這樣寫:

這是因為bool模闆參數混在變參裡,指定預設值也是沒辦法省略true的。

稍微有點複雜的是參數清單的輸出。一個簡單的寫法是這樣:

parameter在析構的時候,析構函數的scope就是bracket的影響範圍,後面的其它顯示内容,都應該被包括在bracket之内,是以bracket需要顯式定義臨時變量bk;

check的調用理由很簡單,因為我們需要顯示出每個參數的具體類型;

最下面是parameter的遞歸調用。在把out_丢進去之前,我們需要思考下具體的顯示效果。是希望列印出(P1, P2, P3)呢,還是(P1 , P2 , P3)?

在這裡我們選擇了逗号之前沒有空格的第一個版本,是以給parameter傳遞的是out_.compact()。

對parameter的代碼來說,看起來不明顯的就是bracket的作用域了,check和parameter的調用其實是被bracket包圍住的。為了強調bracket的作用範圍,同時規避掉莫名其妙的“(void)bk;”手法,我們可以使用lambda表達式來凸顯邏輯:

這樣bracket的作用域一目了然,并且和check、parameter的定義方式保持一緻,同時也更容易看出來out_.compact()的意圖。

好了,有了上面的這些準備工作,寫一個check的T[]特化是很簡單的:

這時對于不指定數組長度的[]類型,輸出結果如下:

當我們開始興緻勃勃的接着追加[N]的模闆特化之前,需要先檢查下cv的檢查機制是否運作良好:

嘗試編譯時,gcc會給我們吐出一堆類似這樣的compile error:

檢查了出錯資訊後,我們會驚訝的發現對于const int[]類型,竟然可以同時比對T const和T[]。

這是因為按照C++标準ISO/IEC-14882:2011,3.9.3 CV-qualifiers,第5款:

“Cv-qualifiers applied to an array type attach to the underlying element type, so the notation “cv T,” where T is an array type, refers to an array whose elements are so-qualified. Such array types can be said to be more (or less) cv-qualified than other types based on the cv-qualification of the underlying element types.”

可能描述有點晦澀,不過沒關系,在8.3.4 Arrays的第1款最下面還有一行批注如下:

“[ Note: An “array of N cv-qualifier-seq T” has cv-qualified type; see 3.9.3. —end note ]”

意思就是對于const int[]來說,const不僅屬于數組裡面的int元素所有,同時還會作用到數組本身上。

是以說,我們不得不多做點工作,把cv限定符也特化進來:

這樣對于加了cv屬性的數組而言,編譯和顯示才是正常的。

接下來,考慮[N],我們需要稍微修改一下上面的CHECK_TYPE_ARRAY__宏,讓它可以同時處理[]和[N]:

這段代碼裡稍微用了點的技巧。gcc的__VA_ARGS__處理其實不那麼人性化。雖然我們可以通過“,##__VA_ARGS__”,在變參為空時消除掉前面的逗号,但這個機制卻隻對第一層宏有效。當我們把__VA_ARGS__繼續向下傳遞時,變參為空逗号也不會消失。

是以,我們隻有用上面這種略顯抽搐的寫法來幹掉第二層宏裡的逗号。這個處理技巧也同樣适用于vc。

然後,實作各種特化模闆的時候到了:

這裡有個有意思的地方是:,也叫“柔性數組”。這玩意在gcc裡不會适配到T[N]或T[]上,是以要單獨考慮。

現在,我們适配上了所有的引用、數組,以及普通指針:

這裡看起來有點不一樣的是多元數組的輸出結果,每個次元都被括号限定了結合範圍。這種用括号明确标明數組每個次元的結合優先級的寫法,雖然看起來不那麼幹脆,不過在C++中也是合法的。

當然,如果覺得這樣不好看,想搞定這個也很簡單,稍微改一下CHECK_TYPE_ARRAY__就可以了:

這裡使用了std::is_array來判斷下一層類型是否仍舊是數組,如果是的話,則不輸出括号。

有了前面準備好的parameter,實作一個函數的特化處理非常輕松:

這裡有一個小注意點:函數和數組一樣,處于被繼承的位置時需要加括号;parameter的構造時機應該在bracket的前面,這樣可以保證它在bracket之後被析構,否則參數清單将被添加到錯誤位置上。

我們可以列印一個變态一點的類型來驗證下正确性:

我們可以看到,函數指針已經被正确的處理掉了。這是因為一個函數指針會适配到指針上,之後去掉指針的類型将是一個正常的函數類型。

這裡我們沒有考慮stdcall、fastcall等調用約定的處理,如有需要的話,讀者可自行添加。

類成員指針的處理非常簡單:

其實我們不用做什麼特别的處理,通過T C::*已經可以适配無cv限定符的普通類成員函數指針了。隻是在vc下,提取出來的T卻無法适配上T(P...)的特化。

這是因為vc中通過T C::*提取出來的函數類型帶上了一個隐藏的thiscall調用約定。在vc裡,我們無法聲明或定義一個thiscall的普通函數類型,于是T C::*的特化适配無法完美的達到我們想要的效果。

是以,我們還是需要處理無cv限定的類成員函數指針。通過一個和上面T C::*的特化很像的特化模闆,就可以處理掉一般的類成員函數指針:

下面考慮帶cv限定符的類成員函數指針。在開始書寫後面的代碼之前,我們需要先思考一下,cv限定符在類成員函數指針上的顯示位置是哪裡?答案當然是在函數的參數表後面。是以我們必須把cv限定符的輸出時機放在T(P...)顯示完畢之後。

是以想要正确的輸出cv限定符,我們必須調整T(P...)特化的調用時機:

上面這段代碼先定義了一個at_destruct,用來在析構時執行“輸出cv限定符”的動作;同時把原本處在基類位置上的T(P...)特化放在了第二成員的位置上,這樣就保證了它将會在cv_之後才被析構。

這裡要注意的是,at_destruct的構造在base_和out_之前,是以如果直接給cv_傳遞out_時不行的,這個時候out_還沒有初始化呢。但是在這個時候,雖然base_同樣尚未初始化,但base_.out_的引用卻是有效的,是以我們可以給cv_傳遞一個base_.out_。

另外,at_destruct雖然定義了帶str參數的構造函數,CHECK_TYPE_MEM_FUNC__宏中卻沒有使用它。原因是若在宏中使用#__VA_ARGS__作為參數,那麼當變參為空時,#__VA_ARGS__前面的逗号在vc中不會被自動忽略掉(gcc會忽略)。

最後,來一起看看輸出效果吧:

折騰C++的類型系統是一個很有意思的事情。當鑽進去之後就會發現,一些原先比較晦澀的基本概念,在研究的過程中都清晰了不少。

check_type的實用價值在于,可以利用它清晰的看見C++中一些隐藏的類型變化。比如完美轉發時的引用折疊:

在上面實作check_type的過程中,用到了不少泛型,甚至元程式設計的小技巧,充分運用了C++在預處理期、編譯期和運作期(RAII)的處理能力。雖然這些代碼僅是學習研究時的興趣之作,實際項目中往往typeid的傳回結果就足夠了,但上面的不少技巧對一些現實中的項目開發也有一定的參考和學習價值。

順便說一下:上面的代碼裡使用了大量C++11的特征。若想在老C++中實作check_type,大部分的新特征也都可以找到替代的手法。隻是适配函數類型時使用的變參模闆,在C++98/03下實作起來實在抽搐。論代碼的表現力和舒适度,C++11強過C++98/03太多了。

完整代碼及測試下載下傳請點選:

更多内容請通路: