Effective Modern C++翻譯(5)-條款4
條款4:了解如何觀察推導出的類型
那些想要知道編譯器推導出的類型的人通常分為兩種,第一種是實用主義者,他們的動力通常來自於軟件產生的問題(例如他們還在調試解決中),他們利用編譯器進行尋找,並相信這個能幫他們找到問題的源頭(they’re looking for insights into compilation that can help them identify the source of the problem.)。另一種是經驗主義者,他們探索條款1-3所描述的推導規則,並且從大量的推導情景中確認他們預測的結果(對於這段代碼,我認為推導出的類型將會是…),但是有時候,他們只是想簡單的回答如果這樣,會怎麼樣呢之類的問題?他們可能想知道如果我用一個萬能引用(見條款26)替代一個左值的常量形參(例如在函數的參數列表中用T&&替代const T&)模板類型推導的結果會改變嗎?
不管你屬於哪一類(二者都是合理的),你所要使用的工具取決於你想要在軟件開發的哪一個階段知道編譯器推導出的結果,我們將要講述3種可行的方法:在編輯代碼的時獲得推導的類型,在編譯時獲得推導的類型,在運行時獲得推導的類型。
IDE編輯器
IDE中的代碼編輯器通常會在你將鼠標停留在程序實體program entities(例如變量,參數,函數等等)上的時候顯示他們的類型。例如,下面的代碼中
const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;
IDE編輯器很可能顯示出x的類型是int,y的類型是const int*.
對於這個工作,你的代碼不能過於復雜,因為是IDE內部的編譯器讓IDE提供了這一項信息,如果編譯器不能充分理解並解析你的代碼,產生類型推導的結果,它就無法告訴你類型推導的結果。
編譯器的診斷
知道編譯器對某一類型推導出的結果一個有效方法是讓它產生一個編譯期的錯誤,因為錯誤的報告肯定會提到導致錯誤的類型。
假如我們想要知道上一個代碼中的x和y被推導出的類型,我們首先聲明卻不定義一個模板,代碼會像下面這樣:
template<typename T> // 只有TD的聲明;
class TD; // TD == "Type Displayer"
嘗試實例化這個模板會產生一個錯誤信息,因為沒有模板的定義,想要查看x和y的類型只需要用它們的類型實例化TD
TD<decltype(x)> xType; // 引起錯誤的信息包括了
TD<decltype(y)> yType; // x和y的leix
// decltype的用法可以參看條款3
我使用這種形式的變量名:variableNameType,因為:它們趨向於產生足夠有用的錯誤信息(I use variable names of the form variableNameType, because they tend to yield quite informative error messages.)對於上面的代碼,其中一個編譯器的錯誤診斷信息如下所示(我突出了我們想要的類型推導結果)
error: aggregate 'TD<int> xType' has incomplete type and
cannot be defined
error: aggregate 'TD<const int *>yType' has incomplete type
and cannot be defined
另一個編譯器提供了一樣的信息,但是格式有所不同
error: 'xType' uses undefined class 'TD<int>'
error: 'yType' uses undefined class 'TD<const int *>'
拋開格式上的不同,我所測試的所有編譯器都提供了包括類型的信息的錯誤診斷信息。
運行時的輸出
利用printf方法(並不是說我推薦你使用printf)顯示類型的信息不能在運行時使用,但是它需要對輸出格式的完全控制,難點是如何讓變量的類型能以文本的方式合理的表現出來,你可能會覺得“沒有問題”typeid和std::type_info會解決這個問題的,你認為我們可以寫下下面的代碼來知道x和y 的類型:
std::cout << typeid(x).name() << '\n'; // 顯示x和y的
std::cout << typeid(y).name() << '\n'; // 類型
這個方法依賴於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<typename T> // 被調用的
void f(const T& param); // 函數模板
std::vector<Widget> createVec(); // 工廠函數
const auto vw = createVec(); // 用工廠函數來實例化vw
if (!vw.empty()) {
f(&vw[0]); // 調用f
}
復制代碼
當你想知道編譯器推導出的類型是什麼的時候,這段代碼更具有代表性,因為它牽涉到了一個用戶自定義類型widget,一個std容器std::vector,一個auto變量,例如,你可能想知道模板參數T的類型,和函數參數f的類型。
使用typeid看起來是非常直接的方法(Loosing typeid on the problem is straightforward.),僅僅是在f中對你想知道的類型加上一些代碼
復制代碼
template<typename T>
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; // 顯示T的類型
cout << "param = " << typeid(param).name() << '\n'; // 顯示參數Param的類型
}
復制代碼
GNU和Clang的執行結果是下面這樣:
T = PK6Widget
param = PK6Widget
我們已經知道PK意味著pointer to const,而6代表了類的名字中有多少個字母(Widget),所以這兩個編譯器告訴了我們T和param的類型都是const Widget*
Morcrosoft的編譯器提供了下面的結果
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<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *
將param的類型顯示為:
const std::_Simple_types<...>::value_type *const &
這個顯示沒有T的那麼嚇人了,中間的…只是意味著IDE告訴你,我將T的類型顯示用…替代了。
template<typename T>
void f(const T& param)
{
TD<T> TType; // elicit errors containing
TD<decltype(param)> paramType; // T's and param's types
…
}
我的理解是大多數顯示在這裡的東西是由於typedef造成的,一旦你通過typedef來獲得潛在的類型信息,你會得到你所尋找的,但需要做一些工作來消除IDE最初顯示出的一些類型,幸運的話, 你的IDE編輯器會對這種代碼處理的更好。
(My understanding is that most of what’s displayed here is typedef cruft and that
once you push through the typedefs to get to the underlying type information,
you get what you’re looking for, but having to do that work pretty much eliminates
any utility the display of the types in the IDE originally promised. With any luck,
your IDE editor does a better job on code like this.)
在我的經驗中,使用編譯器的錯誤診斷信息來知道變量被推導出的類型是相對可靠的方法,利用修訂之後的函數模板f來實例化只是聲明的模板TD,修訂之後的f看起來像下面這樣
template<typename T>
void f(const T& param)
{
TD<T> TType; // 引起錯誤的信息包括了
TD<decltype(param)> paramType; //T和param的類型
}
GNU,Clang和Microsoft的編譯器都提供了帶有T和param正確類型的錯誤信息,當時顯示的格式各有不同,例如在GUN中(格式經過了一點輕微的修改)
error: 'TD<const Widget *> TType' has incomplete type
error: 'TD<const Widget * const &> 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<typename T>
void f(const T& param)
{
#if defined(__GNUC__) //對於GNU和
std::cout << __PRETTY_FUNCTION__ << '\n'; // Clang
#elif defined(_MSC_VER)
std::cout << __FUNCSIG__ << '\n'; //對於Microsoft
#endif
…
}
復制代碼
像之前那樣調用f
std::vector<Widget> createVec(); // 工廠函數
const auto vw = createVec(); // 用工廠函數來實例化vw
if (!vw.empty()) {
f(&vw[0]); //調用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 classWidget*>(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中所描述的類型推導相關的指導方針。