現代C++之理解decltype

 現代C++之理解decltype

decltype用於生成變量名或者表達式的類型,其生成的結果有的是顯而易見的,能夠預測的,容易理解,有些則不容易理解。大多數狀況下,與使用模板和auto時進行的類型推斷相比,decltype做用於變量名或者表達式只是重複了一次變量名或者表達式的確切類型:html

const int i = 0;                         // decltype(i) 爲 const int
bool f(const 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<typename T>                     //  std::vector 的簡易實現
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的主要用於聲明模板函數,此模板函數的返回值類型依賴於其參數類型。例如,看一個例子:咱們須要實現一個模板函數,此模板函數的參數包括一個支持方括號("[]")索引的容器加一個int索引值,中間須要作一些驗證操做,最後函數返回類型應該同容器索引操做的返回類型相同。c++

一個元素類型爲T的容器,operator []的返回值類型應該爲T&。std::queue容器都知足這個要求,std::vector大部分狀況下都知足(std::vector<bool>爲一個例外,operator[]並不返回bool&,而是一個全新的對象),所以注意這裏的容器操做符operator[]的返回值類型依賴於容器類型。函數

使用decltype能夠很方便的實現此模板函數,此模板須要作一些改進,後面討論:性能

template<typename Container, typename Index> // 此函數能夠工做,但能夠改進。
auto authAndAccess(Container& c, Index i) 
-> decltype(c[i]) 
{
    authenticateUser();
    return c[i];
}

注意這裏的auto並無作任何類型推斷,只是用來代表這裏使用的是C++11 的拖尾返回類型(trailing return type)語法,也就是函數返回類型將在參數列表以後進行聲明(在"->"以後),優勢是可使用函數參數來聲明函數返回類型(若是將返回類型放置於函數以前,這裏的參數c和i尚未被聲明,所以不能被使用)。code

C++14中能夠忽略拖尾返回類型了,這樣上面的實現就只剩下auto了。使用這種形式的聲明就意味着要進行類型推斷。編譯器將會根據函數的實現來推斷函數返回類型:htm

template<typename Container, typename Index> // C++14,可是不正確
auto authAndAccess(Container& c, Index i) 
{
    authenticateUser();
    return c[i];//根據c[i]推斷返回類型
}

上一邊帖子的最後解釋了,使用auto做爲函數返回類型,編譯器將會使用模板類型推斷推斷返回類型。這種狀況下上面的函數就有問題了。對於大多數元素類型爲T的容器,operator[]返回T&,可是模板類型推斷
中解釋了,用於初始化的表達式的引用屬性會被忽略掉。看下面的代碼:對象

std::deque<int> d;
…
authAndAccess(d, 5) = 10; // 返回 d[5],賦值10,編譯會出錯

這裏的d[5]會返回int&,可是authAndAccess中的auto返回類型推斷將會把引用剔除掉,最後的返回值類型爲一個右值int。C++中禁止將10賦值給一個右值int,所以編譯失敗。blog

爲了獲得咱們想要的,也就是不使用拖尾返回類型,咱們須要對返回類型使用decltype類型推斷,也就是要指定函數authAndAccess和表達式c[i]返回相同的類型。C++14中咱們使用decltype(auto)標誌符來達到目的。它的意義是:auto代表要進行類型推斷,decltype說明推斷過程當中將會使用decltype推斷規則。最後實實現autoAndAccess以下:索引

template<typename Container, typename Index> // C++14,正確的實現,仍然能夠改進
decltype(auto) authAndAccess(Container& c, Index i) 
{
    authenticateUser();
    return c[i];//根據c[i]推斷返回類型
}

如今authAndAccess將會返回c[i]所返回的。若是c[i]返回一個T&,authAndAccess也會返回T&。若是c[i]返回一個對象,authAccess也會返回一個對象。get

decltype(auto)的使用並不限制於函數返回類型,也可以用於變量的聲明:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto 類型推斷,myWidget1的類型爲 Widget,引用和const屬性被忽略掉了
decltype(auto) myWidget2 = cw;//myWidget2的類型爲const Widget& ,由於這裏使用了decltype推斷推着

在authAndAccess的最後一個版本中,咱們提到了此函數仍然能夠改進,如何作呢?再看一眼函數聲明:

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

這裏函數參數爲按指向非const左值的引用進行傳遞,返回容器中元素的引用到客戶端就容許客戶端對其進行修改。既然是左值引用咱們就不可以向這個函數傳遞右值。可是傳遞右值到函數中多是有意義的,客戶端可能只想得到容器中元素的一份拷貝,看下面的例子:

std::deque<std::string> makeStringDeque(); // 工廠函數
// 獲取從makeStringDeque中返回的deque中第五個元素的拷貝
auto s = authAndAccess(makeStringDeque(), 5);

所以咱們須要對函數進行修訂,使此函數即可以接受左值,也能接受右值。可使用重載(一個函數聲明一個左值引用參數,一個函數聲明一個右值引用參數),可是須要維護兩個函數。咱們可使用universal reference參數類型來避免這種狀況,由於 此參數類型便可以綁定到右值,也能夠綁定到左值,最後authAndAccess能夠聲明成下面這個樣子:

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

在這個模板函數中,咱們不知道須要操做的容器類型,也固然不知道容器內的元素類型,對一個不瞭解其類型的對象採用按值傳遞,可能會帶來沒必要要的拷貝形成的性能問題,還有可能有對象切片問題,可是這裏咱們使用容器索引獲取函數返回值,仿照標準模板庫中的實例來實現看上去是合理的(例如,std::string,std::vector,std::deque,),所以咱們堅持使用按值傳遞。

爲了從返回值中傳遞右值屬性,咱們須要對univversal reference使用std::forward:

template<typename Container, typename Index> // C++14,最終版本
decltype(auto) authAndAccess(Container&& c, Index i) 
{
    authenticateUser();
    return  std::forward<Container>(c)[i];
}

上面的函數須要使用C++ 14的編譯器,若是沒有,也可使用C++11中的模板版本,與C++ 14不一樣的是須要你本身指定返回類型:

template<typename Container, typename Index> // C++14,最終版本
decltype(auto) authAndAccess(Container&& c, Index i) ->decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return  std::forward<Container>(c)[i];
}

咱們還須要說明另一個問題,文章開始說起了,decltype大多數狀況下會返回你所指望的類型,可是還有一些例外,爲了更好的理解decltype,咱們也須要熟悉這些狀況。

將decltype應用於變量名會生成同此變量名相同的類型。這種狀況下沒有例外。可是對於左值表達式來講狀況就有些複雜了,decltype會確保其做用於左值表達式時,生成的類型爲一個左值引用。也就是說若是一個左值表達式(而非變量名)的類型爲T,那麼decltype(左值表達式)的類型就是T&。大多數狀況下這不會有任何影響,由於大多數左值表達式都會顯示的包含一個左值引用標識符。例如,函數返回左值時,一般會返回左值引用,也就包含一個&標識符。

可是有一種狀況須要注意:

int x =0;

x是變量名,所以decltype(x)的類型爲int。可是用括號()將x括起來將會生成一個表達式,表達式(x)也爲左值,所以decltype((x))爲int&。

進一步考慮c++14中的decltype(auto):

decltype(auto) f1()
{
    int x = 0;
    …
    return x; // decltype(x) 爲int,f1返回int
}
decltype(auto) f2()
{
    int x = 0;
    …
    return (x); // decltype((x)) 爲 int&,  f2 返回 int&
}

注意第二種狀況不只僅返回值發生了變化,並且返回的是指向本地變量的引用。所以要警覺這種錯誤的發生。

最後總結一下:

  • 大多數狀況下decltype爲變量名或者表達式生成的類型是不會發生變化的。
  • decltype做用於左值表達式時,生成的類型爲T&。
  • 採用C++14中的decltype(audo)進行類型推斷時,使用decltype推斷規則進行推斷。
相關文章
相關標籤/搜索