天天看點

現代C++之了解decltype 現代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推斷規則進行推斷。

繼續閱讀