Item 4: 知道怎麼去看推導的類型

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

博客已經遷移到這裏啦

對於推導類型結果的查看,根據不同的軟件開發階段,你想知道的信息的不同,可以選擇不同的工具。我們將探討三種可能性:在你編輯代碼時獲得類型推導信息,在編譯期獲得信息,在運行期獲得信息。

IDE 編輯器

在IDE中編輯代碼常常能顯示程序實體(比如,變量,參數,函數等)的類型,只需要你做一些像把光標放在實體上面之類的事。舉個例子,給出這樣的代碼:

const int theAnswer = 42;

auto x = theAnswer;
auto y = &theAnswer;

一個IDE編輯器可能會顯示x的推導類型爲int,y的推導類型爲int*。

爲了像這樣工作,你的代碼肯定或多或少處於編譯狀態,因爲只有C++編譯器(或者至少是一個編譯器前端)在IDE底層運行才能給IDE提供這樣的類型推導信息。如果編譯器不能成功分析和執行類型推導來明確你的代碼,它就不能顯示他推導的類型。

對於一些簡單的類型,比如int,IDE給出的信息通常是對的。然而,就像我們馬上要看到的那樣,當涉及更加複雜的類型時,IDE顯示的信息可能就沒什麼幫助了。

編譯器診斷

這裏有一個有效的方法,讓編譯器顯示它推導的類型,那就是把這個類型用在會導致編譯錯誤的地方。錯誤信息報告錯誤的時候常常會涉及到造成這個錯誤的類型。

假設,爲了舉個例子,我們想看看之前例子中的x和y被推導成什麼類型。我們可以先聲明一個class template但是不去定義它。就好像這樣漂亮的代碼:

template<typename T>    //只是聲明TD
class TD;               //TD == type displayer
                        //類型顯示器

嘗試實例化這個template將引起錯誤,因爲我們沒有相應的定義來實例化。爲了看x和y的類型,只要使用它們的類型嘗試實例化TD:

TD<decltype(x)> xType;  //引起錯誤,錯誤會包含
TD<decltype(x)> yType;  //x和y的類型

我使用variableNameType的形式來命名變量名字,因爲它們在錯誤信息輸出的時候出現,並幫我找到我要找的信息。對於上面的代碼,我的其中一個編譯器的一部分關於類型判斷的輸出就在下面(我已經高亮顯示了類型信息)(譯註:就是int和const int *)

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::name會拯救我們。”在我們接着探討如何查看x和y的類型推導前,你可能覺得我們能像下面這樣寫:

std::cout<<typeid(x).name()<<'\n';  //顯示x的y的類型
std::cout<<typeid(y).name()<<'\n';

這個方法依賴於一個事實,那就是對x和y使用typeid,能產生一個std::type_info的對象,並且這個std::type_info對象有一個成員函數,name,它能產生一個C風格的字符串(也就是一個 const char*)來代替類型的名字。

調用std::type_info::name不能保證返回任何明顯易懂的類型,但是實現儘量保證有用。有用的情況是不同的,GNU和Clang編譯器報告x的類型是“i”,並且y的類型是“PKi”。如果你學過,這些返回信息將是有意義的,來自編譯器的這些輸出,“i”意味着“int”,“PK”意味着“point to konst(諧音const)”(兩個編譯器都支持一個工具,c++filt,這工具能解析這些“殘缺的”類型)。微軟的編譯器產生更明確的輸出:x是“int”,y是“int const*”.

因爲這些x和y的類型結果都是對的,你可能覺得類型顯示的問題已經被解決了,但是別這麼輕率。考慮下更加複雜的例子:

template<typename T>            //需要調用的template函數
void f(const T& param);

std::vector<Widget> createVec();//工廠函數

cosnt auto vw = createVec();    //初始化vw

if(!vw.empty()){
    f(&vw[0]);                  //調用f
    ...
}

這段代碼涉及了一個user-defined的類型(Widget),一個STL的容器(std::vector)和一個auto變量(vw)。這是一個典型的auto類型,你可能想直觀地看一下你的編譯器會推導出什麼類型。例如,如果能看下template參數類型T和函數參數param的類型將非常棒。

使用粗糙的typeid是很直接的,只要加一些代碼到f中來顯示你想看的類型:

template<typename T>
void f(cosnt 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 cosnt”,所以剩下的謎題就是數字6。它簡單地表示class名字(Widget)的長度。所以這些編譯器告訴我們T和param都是const Widget*的類型。

微軟的編譯器同樣:

T       = class Widget cosnt *
param   = class Widget cosnt *   

這三個獨立的編譯器產生了同樣的信息暗示結果是準確的。但是看得再仔細一些,在template f中,param的聲明類型是const T&。這就是問題所在了,T和param的類型是相同的,這看起來不是很奇怪嗎?舉個例子,如果T是int,那麼param應該是const int&,它們並不是一樣的類型。

很不幸,std::type_info::name的結果是不可靠的。在這種情況下,例子中的三個編譯器對類型的解讀都是錯誤的。另外,本質上,它們都應該要是錯誤的,因爲std::type_info::name的說明書上說,傳入的類型會以傳值(by-value)的方式傳入一個template函數。就像item 1解釋的那樣,這意味着如果類型是一個引用,他的引用屬性會被忽略,並且在去掉引用屬性後,是const(或volatile)的,它的const(或volatile)屬性也會被忽略。這就是爲什麼param的類型(cosnt Widget * const&)被報告稱cosnt Widget*。指針的引用屬性和const屬性都被消除了。

同樣不幸的是IDE顯示的類型信息也是不可靠的,或者至少說是沒有用處。在這個例子中,一個我知道的IDE編輯器報告T的類型是(我不是胡編亂造):

cosnt
std::Simple_types<std::Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *

同樣的IDE編輯器顯示param的類型:

const std::_Simple_types<...>::value_type *const &

這沒有T的類型那麼嚇人,但是中間的“…”讓你感到困惑,除非你瞭解到,這個IDE編輯器說出“我省略所有的T表示的東西”。如果運氣好的話,你的開發環境會做一個更好的工作。

如果比起運氣,你更傾向於依賴庫的話,你將會很高興知道std::type_info::name和IDE都失敗的事情,Boost TypeIndex library(常寫作Boost.TypeIndex)卻成功了。這個庫不是標準C++庫的一部分,但是IDE和template TD也不是。另外,事實上Boost庫(可以從boost.com獲取)是跨平臺的,開源的,
只要你在license下設計,即使是最偏執的團隊(要求很高的可移植性)也能很容易設計出漂亮的程序。這意味着代碼中用了Boost庫的可移植性同依賴於標準庫的可移植性是幾乎相同的。

這裏給出我們的函數f怎麼用Boost.TypeIndex產出正確的類型信息:

#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
    using std::cout;
    using boost::typeindex::type_id_with_cvr;
    // show T
    cout << "T = "
         << type_id_with_cvr<T>().pretty_name()
         << '\n';
    // show param's type
    cout << "param = "
         << type_id_with_cvr<decltype(param)>().pretty_name()
         << '\n';
    …
}

這段代碼工作的方式是,function template boost::typeindex::type_id_with_cvr使用一個類型參數(我們想顯示信息的類型)並且不移除const,volatile,或引用屬性(因此template名字後面帶着with_cvr)。結果是一個boost::typeindex::type_index對象,這個對象有一個pretty_name的成員函數,產生一個std::string對象,裏面是我們易讀的並且符合具體類型的字符串。

用這個f的實現,再看看那個用typeid會對param產生錯誤類型信息的函數調用:

std::vector<Widget> createVec();

cosnt auto vw = createVec();

if(!vw.empty()){
    f(&vw[0]);
    ...
}

在GNU和Clang編譯器下,Boost.TypeIndex 產生這個(準確的)輸出:

T       = Widget const*
param   = Widget const* const&

微軟的編譯器產生的結果本質上是相同的:

T       = class Widget const*
param   = class Widget const* const&

這樣的“幾乎一致”是好的,但是記住IDE編輯器,編譯器錯誤消息和像Boost.TypeIndex這樣的庫是幾乎所有的工具你能用來幫助你找出你的編譯器推導出來的類型。但是,最後說一句,沒有任何東西能替代items 1-3中對類型推導的理解。

            你要記住的事
  • 類型的推導常常能在IDE編輯器,編譯器的錯誤消息,Boost TypeIndex庫中看到
  • 一些工具的返回結果可能沒有幫助或不準確,所以理解C++的類型推導規則是必不可少的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章