條款4:了解如何查看推導出的類型
那些想要了解編譯器如何推導出的類型的人通常分為兩個陣營。第一種陣營是實用主義者。他們的動力通常來自於編寫程序過程中(例如他們還在調試解決中),他們利用編譯器進行尋找,並相信這個能幫他們找到問題的根源。第二種是經驗主義者,他們正在探索條款1-3所描述的推導規則。並且從大量的推導情景中確認他們預測的結果(“對於這段代碼,我認為推導出的類型將會是…”),但是有時候,他們只是想簡單的回答如果這樣,會怎麼樣呢之類的問題?他們可能想知道如果我用一個universal reference(見條款26)替代一個左值的常量形參(例如在函數的參數列表中用T&&替代const T&)模板類型推導的結果會改變嗎?
不管你屬於哪一個陣營(二者都是合理的),你所要使用的工具取決於你想要在軟件開發的哪一個階段知道編譯器推導出的結果。我們會闡述3種可行的方法:在編輯代碼的時獲得推導的類型,在編譯時獲得推導的類型,在運行時獲得推導的類型。
IDE編輯器
當你在IDE中的編輯代碼時,在你將鼠標懸停在程序實體(例如變量,參數,函數等等)上的時候,編譯器顯示他們的類型。例如,在下面的代碼中,
const int theAnswer = 42 ;
auto x = theAnswer;
auto y = &theAnswer;
IDE編輯器很可能會顯示出x的類型是int,y的類型是const int*。
對於這項工作,你的代碼不能過於復雜,因為是IDE內部的C++編譯器讓IDE提供了這一項信息。如果編譯器不能充分理解並解析你的代碼,產生類型推導的結果,它就無法給你顯示類型推導的結果。
編譯器的診斷
一個有效的得知編譯器對某一類型推導出的結果方法是讓它產生一個編譯期的錯誤。因為錯誤的報告信息肯定會提到引起錯誤的類型。
假如,我們想要知道上一個代碼中的x和y被推導出的類型。我們首先聲明一個類模板,但是不定義它,代碼會像下面這樣:
template //declaration only for TD
class TD; //TD == "Type Displayer"
試圖實例化這個模板會產生一個錯誤信息,因為沒有模板的定義和實例。為了要查看x和y的類型,僅僅需要用它們的類型實例化TD:
TD xType //elicit errors containing
TD yType //x's and y's types;
//see Item 3 for decltype info
我使用這種形式的變量名:variableNameType,因為:它們趨向於產生足夠有用的錯誤信息。對於上面的代碼,其中一個編譯器的錯誤診斷信息如下所示(我高亮了我們想要的類型推導結果)
error: aggregate 'TD xType' has incomplete type and
cannot be defined
error: aggregate 'TDyType' has incomplete type
and cannot be defined
另一個編譯器提供了一樣的信息,但是格式有所不同:
error: 'xType' uses undefined class 'TD'
error: 'yType' uses undefined class 'TD'
把格式上的不同放到一旁,我所測試的所有編譯器都提供了包括有用的類型錯誤診斷信息。
運行期間的輸出
利用printf方法(並不是說我推薦你使用printf)顯示類型的信息不能在程序運行時期使用,但是它需要對輸出格式的完全控制。難點是如何讓變量的類型能以文本的方式合理的表現出來,你可能會覺得“沒有問題”typeid和std::type_info::name會解決這個問題的。你認為我們可以寫下下面的代碼來知道x和y 的類型:
std::cout << typeid(x).name() << '\n'; // display types for
std::cout << typeid(y).name() << '\n'; // x and y
這個方法依賴於typeid作用於一個對象上時,返回類型為std::type_info這一個事實,type_info有一個叫name的成員函數,提供了一個C風格的字符串(例如 const char*)來表示這個類型的名字。
調用std::type_info的name並不保證返回的東西一定是清楚明了的,但是會盡可能的提供幫助。不同的編譯器提供的程度各有不同,例如:GNU和Clang編譯器將x的類型表示為”i”,將y的類型表示為”PKI”,一旦你了解i意味著int,pk意味著pointer to Konst const(兩個編譯器都提供一個C++ filt工具,來對這些重整後的名字進行解碼),理解編譯器的輸出將變得容易起來,Microsoft的編譯器提供了更清楚的輸出,x的類型是int,y的類型是int const*.
因為對x和y顯示的結果是正確的,你可能會認為問題已經解決了,但是我們不能草率。想想下面這個更復雜的例子:
template // template function to
void f(const T& param); // be called
std::vector createVec(); // factory function
const auto vw = createVec(); // init vw w/factory return
if (!vw.empty()) {
f(&vw[0]); // call f
}
當你想知道編譯器推導出的類型是什麼的時候,這段代碼更具有代表性,因為它牽涉到了一個用戶自定義類型widget,一個std容器std::vector,一個auto變量,例如,你可能想知道模板參數T的類型,和函數參數f的類型。
使用typeid看起來是非常直接的方法,僅僅是在f中對你想知道的類型加上一些代碼:
template
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; // show T
cout << "param = " << typeid(param).name() << '\n'; // show param's type
...
}
GNU和Clang的執行結果是下面這樣:
T = PK6Widget
param = PK6Widget
我們已經知道PK意味著pointer to const,而6代表了類的名字中有多少個字母(Widget),所以這兩個編譯器告訴了我們T和param的類型都是const Widget*
微軟的編譯器提供了下面的結果
T = class Widget const *
param = class Widget const *
這三個編譯器都提供了一樣的信息,這或許暗示了結果應該是准確的,但是讓我們看的更細致一點,在模板f中,param的類型被聲明為constT&,既然如此的話,param和T的類型一樣難道不讓人感到奇怪嗎,如果T的類型是int,param的類型應該是const int&,看,一點都不一樣。
令人悲哀的是std::type_info::name的結果並不是可依賴的,在這個例子中,三個編譯器對於param的結果都是不正確的,此外,它們必須是錯誤的,因為標准(specification)規定被std::type_info::name處理的類型是被按照按值傳遞給模板對待的,像條款1解釋的那樣,這意味著如果類型本身是一個引用的話,引用部分是被忽略掉的,如果引用去掉之後還含有const,常量性也將被忽略掉,,這就是為什麼const Widget* const &的類型被顯示為const Widget*,首先類型的引用部分被忽略了,接著結果的常量性也被忽略了。
同樣令人傷心的是,IDE提供的類型信息同樣也是不可靠的,或者說不是那麼的實用,對於這個例子,我所知道的編譯器將T的類型顯示為(這不是我編造出來的):
const
std::_Simple_types >::_Alloc>::value_type>::value_type *
將param的類型顯示為:
const std::_Simple_types<...>::value_type *const &
這個顯示沒有T的那麼嚇人了,中間的…只是意味著IDE告訴你,我將T的類型顯示用…替代了。
我的理解是大多數顯示在這裡的東西是由於typedef造成的,一旦你通過typedef來獲得潛在的類型信息,你會得到你所尋找的,但需要做一些工作來消除IDE最初顯示出的一些類型,幸運的話, 你的IDE編輯器會對這種代碼處理的更好。
在我的經驗中,使用編譯器的錯誤診斷信息來知道變量被推導出的類型是相對可靠的方法,利用修訂之後的函數模板f來實例化只是聲明的模板TD,修訂之後的f看起來像下面這樣
template
void f(const T& param)
{
TD TType; // elicit errors containing
TD paramType; // T's and param's types
…
}
GNU,Clang和Microsoft的編譯器都提供了帶有T和param正確類型的錯誤信息,當時顯示的格式各有不同,例如在GUN中(格式經過了一點輕微的修改)
error: 'TD TType' has incomplete type
error: 'TD paramType' has incomplete
type
除了typeid
如果你想要在運行時獲得更正確的推導類型是什麼,我們已經知道typeid並不是一個可靠的方法,一個可行的方法是自己實現一套機制來完成從一個類型到它的表示的映射,概念上這並不困難,你只需要利用type trait和模板元編程的方法來將一個完整類型拆分開(使用std::is_const,std::is_ponter,std::is_lvalue_reference之類的type trait),你還需要自己完成類型的每一部分的字符串表示(盡管你依舊需要typeid和std::type_info::name來產生用戶自定義格式的字符串表達)。
如果你經常需要使用這個方法,並且認為花費在調試,文檔,維護上的努力是值得的,那麼這是一個合理的方法(If you’d use such a facility often enough to justify the effort needed to write, debug,document, and maintain it, that’s a reasonable approach),但是如果你更喜歡那些移植性不是很強的但是能輕易實現並且提供的結果比typeid更好的代碼的, 你需要注意到很多編譯器都提供了語言的擴展來產生一個函數簽名的字符串表達,包括從模板中實例化的函數,模板和模板參數的類型。
例如,GNU和Clang都支持PRETTY_FUNCTION,Microsoft支持了FUNCSIG,他們代表了一個變量(在 GNU和Clang中)或是一個宏(在Microsoft中),如果我們將模板f這麼實現的話
template
void f(const T& param)
{
#if defined(__GNUC__) //For GNU and
std::cout << __PRETTY_FUNCTION__ << '\n'; // Clang
#elif defined(_MSC_VER)
std::cout << __FUNCSIG__ << '\n'; //For Microsoft
#endif
…
}
像之前那樣調用f,
std::vector createVec(); // factory function
const auto vw = createVec(); // init vw w/factory return
if (!vw.empty()) {
f(&vw[0]); //call f
...
}
在GNU中我們得到了以下的結果
void f(const T&) [with T = const Widget*]
告訴我們T的類型被推導為const Widget*(和我們用typeid得到的結果一樣,但是前面沒有PK的編碼和類名前面的6),同時它也告訴我們f參數類型是const T&,如果我們按照這個格式擴展T,我們得到f的類型是const Widget * const&,和typeid的答案不同,但是和使用未定義的模板,產生的錯誤診斷信息中的類型信息一致,所以它是正確的。
Microsoft的 FUNCSIG提供了以下的輸出:
void __cdecl f(const class Widget *const &)
尖括號裡的類型是T被推導的類型,為const Widget*。和我們用typeid得到的結果一樣。括號內的類型是函數參數的類型,是const Widget* const&,和我們用typeid得到的結果不一樣,但同樣和我們使用TD在編譯期得到的類型信息一致。
Clang的PRETTY_FUNCTION,盡管使用了和GNU一樣的名字,但是格式卻和GNU或是Microsoft的不一樣:
void f(const Widget *const &)
它直接顯示出了參數的類型,但是需要我們自己去推導出T的類型被推導為了const Widget*(或者我們也可以通過typeid來獲得T的類型)
IDE編輯器,編譯器的錯誤診斷信息,typeid和PRETTY_FUNCTION,FUNCSIG之類的語言擴展僅僅只是幫助你弄明白編譯器推導出的結果是什麼。但是最後,沒有什麼能替代條款1-3中所描述的類型推導相關的推導規則。
請記住:
?可以通過使用IDE編譯器、編譯錯誤信息、typeid、PRETTY_FUNCTION和FUNCSIG這樣的語言擴展等,查看類型推導。
?一些工具提供的類型推導結果可能既沒有用也不准確,所以理解C++類型推導的原則十分必要。
==============================================================
譯者注釋:
IDE 即Integrated Development Environment,是“集成開發環境”的英文縮寫,可以輔助開發程序的應用軟件。