現代C++之理解decltype

 現代C++之理解decltype

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

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索引值,中間需要做一些驗證操作,最後函數返回類型應該同容器索引操作的返回類型相同。

一個元素類型爲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還沒有被聲明,因此不能被使用)。

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

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,因此編譯失敗。

爲了得到我們想要的,也就是不使用拖尾返回類型,我們需要對返回類型使用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也會返回一個對象。

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推斷規則進行推斷。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章