Item 3: 理解decltype

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

博客已經遷移到這裏啦

decltype是一個奇怪的東西。給出一個名字或者一個表達式,decltype可以告訴你名字或表達式的類型。大多情況下,他告訴你的就是確實你想的那樣。但是偶爾,他會提供一個脫離你想象的結果,這導致了你必須去找一本參考書或者去在線Q&A網站尋求答案。

我們從一般情況(沒有意外的結果)開始。對比template和auto的類型推導,decltype模仿你給的名字或表達式:

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

bool f(cosnt Widget& w);    //decltype(w)是const Widget&
                            //decltype(f)是bool(const Widget&)

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

Widget w;                   //decltype(w)是Widget

if(f(w))...                 //decltype(f(w))是bool

template<template T>
class vector{
public:
    ...
    T& operator[](std::size_t index);
    ...
};

vector<int> v;              //decltype(v)是vector<int>
...
if(v[0] == 0)...            //decltype(v[0])是int&

看吧,沒有任何的意外之處。

在C++11中,也許decltype最主要的用途就是用來聲明 返回值類型依賴於參數類型的 函數模板。舉個例子,假設你要寫一個函數,這個函數需要一個支持下標操作(”[]”)的容器,然後根據下標操作來識別出用戶,函數的返回值類型應該和下標操作的類型相同。

以T爲元素類型的容器,operator[]操作通常返回T&,這是std::deque的情況,比方說,這是std::vector的大多數情況。然而,對於std::vector,operator[]操作不返回bool&。取而代之的,它返回一個表示同樣值的新對象,對於這種情況的討論將放在item 6,但是在這裏,重點是:operator[]函數返回的類型取決於容器的類型。

decltype讓表達式變得簡單。我們將寫下第一個template,展示如何用decltype計算返回值。template可以進一步精煉,但是我們先寫成這樣:

template<typename Container,typename Index>
auto authAndAccess(Container& c, Index i)   //需要精煉
    ->decltype(c[i])
{
    authenticateUser();
    return c[i];
}

函數前面的那個auto的使用沒有做任何類型的推導。當然了,這標誌着C++11使用了返回值類型後置的語法。也就是,函數的返回值類型將跟在參數列表(”->”的購買)後面聲明。一個後置的返回類型擁有一個優點,就是函數的參數能用來確定返回值類型。在authAndAccess中,爲了舉例,我們用c和i來明確函數的返回值類型。按傳統的使用方法,我們讓返回值類型處於函數名的前面,那麼c和i就不能使用了,因爲解析到這時,他們還沒被聲明出來。

使用這個聲明方式,就和我們的需求一樣,authAndAccess根據我們傳入的容器,以這個容器的operator[]操作返回的類型來作爲它自身的返回值類型。

C++11允許我們推導一些比較簡單的lambdas表達式的返回類型,並且在C++14中,把這種推導擴張到了所有的lambdas表達式和所有的函數中,包括那些有多條語句的複雜的函數。在authAndAccess中,這意味着在C++14中,我們能忽略返回值類型,只需使用auto即可。在這樣的聲明形式下,auto意味着類型推導將會發生。尤其是,它意味着編譯器將會根據函數的實現來推導函數的返回值類型。

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) //需要精煉
{
    authenticateUser();
    return c[i];
}

Item 2解釋了一個返回auto的函數,編譯器採用template的類型推導規則。在這種情況下,這是有問題的。就像我們討論的那樣,對於大多數容器,operator[]返回的是T&,但是Item 1解釋了在template類型推導中,表達式的引用屬性會被忽略(情況3),考慮下這對於客戶代碼意味着什麼:

std::deque<int> d;
...
authAndAccess(d, 5) = 10;   //這會返回d[5],然後把10賦給它。
                            //但是這會出現編譯錯誤(給一個右值賦值)

這裏,d[5]返回一個int&,但是對authAndAccess的auto返回類型的推導將去掉引用屬性,這產生了一個int類型,int是函數的返回值類型,是一個右值,然後這段代碼嘗試給一個右值int賦值一個10.這在C++中是禁止的,所以代碼無法編譯。

爲了讓authAndAccess能工作地像我們希望的那樣,我們需要對返回值類型使用decltype類型推導,也就是明確authAndAccess應該返回表達式c[i]返回的類型。C++的規則制定者,預料到了在一些類型需要推測的情況下,用decltype類型推導規則來推導的需求,所以在C++14中,通過decltype(auto)類型說明符來讓之成爲可能。這一開始看起來可能有點矛盾的東西(decltype和auto)確實完美地結合在一起:auto明確了類型需要被推導,decltype明確了推導時使用decltype推導規則。因此我們像這樣能寫出authAndAccess的代碼:

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i) //需要精煉
{
    authenticateUser();
    return c[i];
}

現在authAndAccess能真實地返回c[i]返回的類型了。尤其是對於一些c[i]返回一個T&的情況,authAndAccess也會返回一個T&,並且在不尋常的情況下,當c[i]返回一個對象,authAndAccess也會返回一個對象。

decltype(auto)的使用不止侷限於函數返回類型,當你對正在初始化的表達式使用decltype類型的類型推導規則時,它也能很方便地用在變量的聲明上:

Widget w;

cosnt Widget& cw = w;

auto myWidget1 = cw;    //myWidget1的類型是Widget

decltype(auto) myWidget2 = cw
                        //myWidget2的類型是const Widget&

我知道現在有兩件事困擾着你,一個是我上面提到卻沒有討論的對於authAndAccess的精煉,現在讓我們處理它:

再一次看一下C++14版本的authAndAccess的聲明:

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

容器以非const左值引用(lvalue-reference-to-non-cosnt)的方式傳入,因爲一個對元素的引用允許客戶更改容器,但是這意味着我們不能傳一個rvalue容器給這個函數。rvalue不能和lvalue引用綁定(除非他們是const左值引用(lvalue-references-to-const),但是這裏不是)。

公認地,傳一個rvalue的容器給authAndAccess是很罕見的情況。一個rvalue容器是一個臨時對象,它在authAndAccess的調用語句結束的時候就會被銷燬,這意味着對這樣一個容器(authAndAccess的返回值)的引用在語句結束時會產生未定義的結果。但是,傳遞一個臨時變量給authAndAccess還是有意義的。一個客戶可能簡單地想產生臨時容器中元素的一份拷貝,舉個例子:

std::deque<std::string> makeStringDeque();

//拷貝一份makeStringDeque返回的deque的下標爲5的元素
auto s = authAndAccess(makeStringDeque(), 5);

爲了支持這樣的使用,意味着我們需要修改authAndAccess的聲明,讓它能同時接受lvalues和rvalues。重載可以很好的工作(一個接受lvalue引用參數的函數,一個接受rvalue引用參數的函數),但是這樣的話會我們需要維護兩個函數。一個避免這樣的方法是使用一個引用參數(universal引用),它能同時接受lvalues和rvalues,在Item 24中會解釋universal引用具體做了什麼。因此authAndAccess能聲明成這樣:

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

在這個template中,我們不知道容器的類型,所以,這意味着我們同樣不知道容器對象使用的索引類型。對於未知對象使用傳值(pass-by-value)方式通常會因爲沒必要的拷貝而對效率產生很大的影響,以及對象切割(Item 41)的問題,並會被我們的同事嘲笑,但是在容器索引的使用上,跟隨標準庫(比如,std::string,std::vector以及std::deque的operator[])的腳步看起來是合理的。所以我們將堅持以傳值(pass-by-value)的方式使用它。

然而,我們需要使用std::forward更新template對universal引用的實現來讓它符合Item 25的告誡。

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container&& c, Index i)
{
    authnticateUser();
    return std::forward<Container>(c)[i];
}

這個例子應該會做到所有我們想要的事情了,但是它需要C++14的編譯器。如果你沒有,你將需要使用C++11版本的template。除了你需要自己明確返回值類型外,它和C++14的版本是一樣的:

template<typename Container, typename Index>
auto
authAndAccess(Container&& c, Index i)
    -> decltype(std::forward<Container>(c)[i])
{
    authnticateUser();
    return std::forward<Container>(c)[i];
}

另一個可能糾纏你的問題是我在一開始的評論,我說decltype在大多數情況下回產生你想要的類型,幾乎不會帶來意外。老實說,你不太可能遇到這些例外規則除非你是一個重大的庫的實現者。

爲了完全理解decltype的行爲,你必須讓你自己熟悉一些特殊情況。大多數這些情況都太隱晦了所以不會確切地在書中討論,但是爲了看清楚decltype的內部情況以及他的使用需要藉助這種情況。

應用decltype來產生一個名字(name)的類型,如果名字(name)是左值表達式,不影響decltype的行爲。比起名字(names)lvalue表達式更加複雜,然而decltype保證推導的類型總是lvalue引用。那就是,一個lvalue表達式除了name會推導出T,其他情況decltype推導出來的類型就是T&。這很少有影響,因爲大多數lvalue表達式本質上包含一個lvalue引用的修飾符。比如函數返回lvalues時,它總是返回左值引用。

這裏有一個隱含的行爲值得你去意識到,在

int x = 0;

中x是一個變量的名字,所以decltype(x)是int,但是如果把x包裝在括號中—”(x)”—將產生一個比名字更復雜的表達式。作爲一個名字,x是一個lvalue,並且C++定義表達式(x)也是一個左值。decltype((x))因此是int&。把括號放在name的兩旁將改變decltype推導出來的類型。

在C++11中,沒什麼好奇怪的,但是結合C++14對decltype(auto)的支持,這意味着你在函數中寫的返回語句將影響到函數類型的推導:

decltype(auto) f1()
{
    int x = 0;
    ...
    return x;   //decltypex(x)是int,所以f1返回int
}

decltype(auto) f2()
{
    int x = 0;
    ...
    return (x); //decltype((x))是int&,所以f2返回int&
}

記住f2不僅僅是返回值和f1不同,它還返回了一個局部變量。這是一種把你推向未定義行爲的陷阱代碼。

最重要的教訓就是,在使用decltype(auto)的時候小心再小心。在表達式中,對類型的推導看起來無關緊要的細節能影響到decltype(auto)的推導。爲了保證類型的推導和你想的一樣,請使用Item 4描述的方法。

同時,不要忽視大局。當然,decltype(包括單獨使用以及和auto一起使用)可能偶爾產生出意外的類型,但是那不是通常的情況。通常,decltype產生你想要的類型。當decltype應用在name時,它總產生你想要的情況,在這情況下,decltype就像聽起來那樣:它推導出name的聲明類型。

            你要記住的事
  • decltype大多數情況下總是不加修改地產出變量和表達式的類型
  • 對於T類型的lvalue表達式,decltype總是產出T&。
  • C++14支持decltype(auto),和auto一樣,他從一個初始化器中推導類型,只不過使用的是decltype類型推導規則。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章