程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 《Effective Modern C++》翻譯--條款3: 理解decltype

《Effective Modern C++》翻譯--條款3: 理解decltype

編輯:C++入門知識

《Effective Modern C++》翻譯--條款3: 理解decltype


條款3:理解decltype

decltype 是一個非常有趣的怪獸。如果提供了一個名字或是表達式,decltype關鍵字將會告訴你這個名字或這個表達式的類型。通常情況下,結果與你的期望吻合。然而有些時候,decltype產生的結果領你撓頭,使你去翻閱參考書或在網上問答中尋求答案。

我們先從通常的情況開始—這裡沒有暗藏驚喜。聯系到在模板類型推導和auto類型推導中發生了什麼,decltype關鍵字就像鹦鹉學舌一樣,對於變量名稱或表達式類型的推導跟模板類型推導和auto類型推導沒有任何變化:

const int i = 0;           //decltype(i) is const int

bool f(const Widget& w);   //decltype(w) is const Widget&
                           //decltype(f) is bool(const Widget&)

struct Point{
  int x, y;                //decltype(Point::x) is int
};                         //decltype(Point::y) is int

Widget w;                  //decltype(w) is Widget

if (f(W)) ...              //decltype(f(w)) is bool

template       //simplified version fo std::vector
class vector{
public:
  ...
  T& operator[](std::size_t index);
  ...
};

vectorv;               //decltype(v) is vector
...
if(v[0] == 0)...            //decltype(v[i]) is int&

看到了吧,毫無驚喜。

在C ++11,對於decltype的主要用途是聲明函數模板,其中函數的返回類型依賴於它的參數類型。例如,假設我們想編寫一個函數,它支持通過方括號(即使用“[]”)加一個索引,然後進行身份驗證,然後再重新打開索引的結果,用戶的容器操作。該函數的返回類型應該與索引操作返回的類型相同。

[]運算符作用在一個以T為元素的容器上時,通常返回T&,std::deque就是這樣的,std::vector也幾乎一樣。唯一的例外是對於std::vecotr,[]運算符不返回一個bool&。相反的,它返回一個全新的對象,條款6將解釋這是為什麼,但是重要的是記住作用在容器上的[]運算符的返回類型取決於這個容器本身。

decltype讓這件事變得簡單。這裡是我們寫的第一個版本,顯示了使用decltype推導返回類型的方法。這個模板還可以再精簡一些,但是我們暫時先不這麼干:

template     //works, but
auto autoAnadAccess(Container& c, Index i)       //requires
  -> decltype(c[i])                              //refinement
{
  authenticateUser();
  return c[i];
}

函數名字前的auto對於類型推導結果毫無相關。它暗示了C++11的追蹤返回類型(trailing return type)語義正被使用,例如:函數的返回類型將在參數列表的後面聲明(在->之後)。追蹤返回類型的優勢是函數的參數能在返回類型的聲明中使用。例如,在authAndAccess中,我們用c和i來指定函數的返回類型,如果我們想要將返回類型聲明在函數名前面,就像傳統的函數一樣,c和i是不能被使用的,因為他們還沒有被聲明。

使用這個聲明,正如我們期望的那樣,authAndAccess返回[]運算符作用在容器上時的返回類型。

C++11允許推導單一語句的lambda的返回類型,C++14擴展了這個,使得lambda和所有函數(包括含有多條語句的函數)的返回類型都可以推導。這意味著在C++14中我們可以省略掉追蹤返回類型(trailing return type),只留下auto,在這種形式下的聲明中,auto意味著類型推導將會發生。特別的,它意味著編譯器將會從函數的實現來推導函數的返回類型:

template   //C++14 only, and
auto authAndAccess(Container&c, Index i)       //not quite
{
  authenticateUser();
  return c[i];                                 //return type deduced from c[i]
}

但是哪一種C++的類型推導規則將會被使用呢?模板的類型推導規則還是auto的,或者是decltype的?

也許令人感到吃驚,帶有auto返回類型的函數使用模板類型推導規則。盡管看起來auto的類型推導規則會更符合這個語義,但是模板類型推導規則和auto類型推導規則幾乎是一模一樣的,唯一的不同是模板類型推導規則在面對大括號的初始化式時會失敗。

既然這樣的話,使用模板類型推導規則推導authAndAccess的返回類型是有問題的,但是auto類型推導規則也好不了多少,困難源自他們對左值表達式的處理。

像我們之前討論過的,大多數[]運算符作用在以T為元素的容器上時返回一個T&,但是條款1解釋了在模板類型推導期間,初始化表達式的引用部分將被忽略掉,考慮下面的客戶代碼,使用了帶有auto返回類型(使用模板類型推導來推導它的返回類型)的authAndAccess:

std::deque d;
...
authAndAccess(d, 5) = 10;    //authenticate user, return d[5], then assign 10 to it;
                             //this won't compile

這裡,d[5]會返回int&,但是用auto類型推導來推導函數authAndAccess,將會去掉引用,變成返回一個int類型。這樣,這個int返回值就成為了函數的返回值,是一個右值,然後上面的代碼試圖將10賦給一個右值。在C++中,這樣的賦值是被拒絕的,所以上面的代碼不會編譯成功。

問題源於我們使用的是模板類型推導規則,它會丟棄初始化表達式中的引用限定符。所以在這種情況下,我們想要的是decltype類型規則,decltype類型推導能允許我們確保authAndAccess返回的類型和表達式c[i]類型是完全一致的。

C++規則的制定者,預料到了在某種情況下類型推導需要使用decltype類型推導規則,所以在C++14中出現了decltype(auto)說明符,這個剛開始看起來可能會有些矛盾(decltype和auto?)。但事實上他們是完全合理的,auto說明了類型需要被推導,decltype說明了decltype類型推導應該在推導中被使用,因此authAndAccess的代碼會是下面這樣:

template    //C++14 only;
decltype(auto)                                  //works, but
autoAndAccess(Container&c, Index i)             //still requires
{                                               //refinement
  authenticateUser();
  return c[i];
}

這樣,函數autoAndAccess的返回類型與c[i]保持一致。特別強調的是,當c[i]返回一個T&時,authAndAccess也會返回一個T&,而當c[i]返回一個對象時,authAndAccess也會返回一個對象。

decltype(auto)的使用並不局限於函數的返回類型,當你想要用decltype類型推導來推導初始化式時,你也可以很方便的使用它來聲明一個變量:

Widget w;

const Widget& cw = w;

auto myWidget1 = cw;            //auto type deduction;
                                //myWidget1's type is Widget

decltype(auto) myWidget2 = cw;  //decltype type deduction:
                                //myWidget2's type is
                                // const Widget&

但是我知道,這時有兩件事困擾著你。一個是我之前提到的為什麼authAndAccess仍需要改進,現在讓我們補上這一段吧。

我們再看一次C++14版本下的authAndAccess函數聲明:

template 
decltype(auto) authAndAccess(Container& c, Index i);

容器是以一個左值的非常量引用傳入的,因為返回一個容器中元素的引用允許我們來修改這個容器,但這意味著我們不可能傳遞一個右值的容器到這個函數中去,右值是無法綁定到一個左值的引用上的(除非是一個的常量左值引用,但本例中不是這樣的)

無可否認,傳遞一個右值的容器給authAndAccess是一個邊界情況,一個右值的容器,作為一個臨時對象將會在包含authAndAccess的函數調用的語句結束後被摧毀。這意味著容器中的一個元素的引用(這通常是authAndAccess函數返回的)將會在調用語句的結束時懸空。然而,傳遞一個臨時對象到authAndAccess中是有道理的,一個客戶可能只是想要拷貝這個臨時容器中的一個元素,例如:

std::deque makeStringDeque();     //factory function

//make copy of 5th element of deque returned
//from makeStringDeque
auto s = authAndAccess(makeStringDeque(), 5);

為了支持這種使用方法,意味著我們需要修改c的聲明使它可以同時接受左值和右值;這意味著c需要成為universal reference(見條款26)

template 
decltype(auto) authAndAccess(Container&& c, Index i);

在上面的模板裡,我們不知道我們操作的容器是什麼類型的,同時也意味著我們忽略了容器下標所對應的元素的類型。利用傳值方式傳遞一個未知的對象,通常需要忍受不必要的拷貝,對象被分割的問題(見條款17),還有來自同事的嘲笑。但是根據標准庫中的例子(例如 std::string,std::vector和std::deque),這種情況下看起來也是合理的,所以我們仍然堅持按值傳遞。

現在要做的就是更新模板的實現,根據條款27中的警告,使用std::forward來實現universal reference:


template       // final
decltype(auto)                                     // C++14  authAndAccess(Container&& c, Index i)              // version  
{ 
  authenticateUser(); 
  return std::forward(c)[i]; 
}

上面代碼完成了我們想要的一切,但前提是需要支持C++14的編譯器。如果你沒有支持C++14的編譯器,你就應該使用C++11中的模板類型。除了你需要自己明確出返回類型外,其他的與C++14沒有區別:


template       // final 
auto                                               // C++11  authAndAccess(Container&& c, Index i)              // version   
-> decltype(std::forward(c)[i]) 
{ 
  authenticateUser(); 
  return std::forward(c)[i]; 
}

另一個值得對你唠叨的問題我已經標注在了這一條款的開始處了,decltype的結果幾乎和你所期待的一樣,這已經不足為奇了。說實話,除非你要實現一個非常龐大的庫,否則你幾乎不太可能遇到這個規則的例外情況,

為了完全理解decltype的行為,你需要讓你自己熟悉一些特殊的情況。大多數在這本書裡證明討論起來會非常的晦澀,但是其中一條能讓我們更加理解decltype的使用。

但是就像我說的,對一個變量名使用decltype產生聲明這個變量時的類型。有名字的是左值表達式,但這沒有影響decltype的行為。因為對於比變量名更復雜的左值表達式,decltype確保推導出的類型總是一個左值的引用。這意味著如果一個左值表達式不同於變量名的類型T,decltype推導出的類型將會是T&。這幾乎不會照成什麼影響,因為大多數左值表達式的類型內部通常包含了一個左值引用的限定符。例如,返回左值的函數總是返回一個引用。

這裡有一個值得注意的地方,在

int x=0;

中x是一個變量的名字,所以decltype(x)的結果是int。但是將名字x用括號包裹起來,”(x)”產生了一個比名字更復雜的表達式,作為一個變量名,x是一個左值,C++同時定義了(x)也是一個左值,因此decltype((x))結果是int&。通過將一個變量用括號包裹起來改變了decltype最初的結果。

在C++11中,這僅僅會讓人有些奇怪,但是結合C++14中對decltype(auto)的支持後,你對返回語句的一些簡單的變化會影響到函數最終推導出的結果:

decltype(auto) f1()  
{ 
  int x = 0;
  …
  return x;        // decltype(x) is int, so f1 returns int 
} 

decltype(auto) f2() 
{ 
  int x = 0;
  … 
  return (x);      // decltype((x)) is int&, so f2 returns int& 
}

注意到f2和f1不僅僅是返回類型上的不同,f2返回的是一個局部變量的引用!,這種代碼的後果是造成未定義的行為,這當然不是你希望發生的情況。

這裡主要講的是使用decltype(auto)。但需要格外注意,一些看起來無關緊要的細節會影響到decltype(auto)推導出的結果。為了確保被推導出的類型如你期待, 可以使用條款4中描述的技術。

但同時,不要失去對大局的關注。decltype(無論是單獨使用還是和auto一起使用)推導的結果可能偶爾令人吃驚,但是這並不會經常發生。通常,decltype的結果和你所期待的類型一樣,尤其是當decltype應用在變量名的時候,因為在這種情況下,decltype做的就是提供變量的聲明類型。

請記住:

?decltype一般情況下總是返回變量名或是表達式的類型而不會進行任何的修改。

?對於不同於變量名的左值表達式,decltype的結果總是T&。

?C++14提供了decltype(auto)的支持,比如auto,從它的初始化式中推導類型,但使用的是decltype的推導規則。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved