筆記-1-Deducing Type

C++類型推斷

對於靜態語言來講,你通常要明確告訴編譯器變量或者表達式的類型。可是慶幸地是,如今C++已經引入了自動類型推斷:編譯器能夠自動推斷出類型。在C++11以前,類型推斷只是用在模板上。而C++11經過引入兩個關鍵字autodecltype擴展了類型推斷的應用。C++14更進一步擴展了autodecltype的應用範圍。明顯地,類型推斷能夠減小不少無必要的工做。可是高興之餘,你仍然有可能會犯一些錯誤,若是你不能深刻理解類型推斷背後的規則與機理。所以,咱們分別從模板類型推斷、autodecltype的使用三個方面深刻講解類型推斷。數組

模板類型推斷

模板類型推斷在C++98中就已經引入了,它也是理解autodecltype的基石。下面是一個函數模板的通用例子:函數

template <typename T> void f(ParamType param); f(expr); // 對函數進行調用 

編譯器要根據expr來推斷出TParamType的類型。特別注意的是,這兩個類型有可能並不相同,由於ParamType可能會包含修飾詞,好比const&。看下面的例子:ui

template <typename T> void f(const T& param); int x = 0; f(x); // 使用int類型調用函數 

此時類型推斷結果是:T的類型是int,可是ParamType的類型倒是const int&。因此,兩個類型並不相同。還有,你可能很天然地認爲T的類型與表達式expr是同樣的,好比上面的例子:二者是同樣的。可是實際上這也是誤區:T的類型不只取決於expr,也與ParamType牢牢相關。這存在三種不一樣的情形:spa

情形1:ParamType是指針或者引用類型

最簡單的狀況ParamType是指針或者引用類型,但不是通用引用類型(&&)。此時,類型推斷要點是:指針

  1. 若是expr是引用類型,那就忽略引用部分;
  2. 經過相減exprParamType的類型來決定T的類型。

好比,下面是引用類型的例子:code

template <typename T> void f(T& param); // param是引用類型 int x = 27; // x是int類型 const int cx = x; // cx是const int類型 const int& rx = x; // rx是const int&類型 f(x); // 此時T爲int,而param是int& f(cx); // 此時T爲const int,而param是const int& f(rx); // 此時T爲const int,而param是const int& 

其中能夠看到,const對象傳遞給接收T&參數的函數模板時,const屬性是可以被T所捕獲的,即const稱爲T的一部分。同時,引用類型對象的引用屬性是能夠忽略的,並無被T所捕獲。上面處理的實際上是左值引用,對於右值引用,規則是相同的,可是右值引用的通配符T&&還有另外的含義,會在後面講。對象

若是param是常量引用類型,推斷也是類似的,儘管有些區別:索引

template <typename T> void f(const T& param); // param是常量引用類型 int x = 27; // x是int類型 const int cx = x; // cx是const int類型 const int& rx = x; // rx是const int&類型 f(x); // 此時T爲int,而param是const int& f(cx); // 此時T爲int,而param是const int& f(rx); // 此時T爲int,而param是const int& 

指針類型也一樣適用:ip

template <typename T> void f(T* param); // param是指針類型 int x = 27; // x是int int* px = &x; // px是int* const int* cpx = &x; // cpx是const int* f(px); // 此時T是int,而param是int* f(cpx); // 此時T是const int,而param是const int* 

顯然,這種情形類型推斷很容易。get

情形2:ParamType是通用引用類型(&&)

這種情形有點複雜,由於通用引用類型參數與右值引用參數的形式是同樣的,可是它們是有區別的,前者容許左值傳入。類型推斷的規則以下:

  1. 若是expr是左值,TParamType都推導爲左值引用,儘管其形式上是右值引用(此時僅把&&匹配符,一旦匹配是左值引用,那麼&&能夠忽略了)。
  2. 若是expr是右值,能夠當作情形1的右值引用。

規則有點繞,仍是例子說話:

template <typename T> void f(T&& param); // 此時param是通用引用類型 int x = 10; // x是int const int cx = x; // cx是const int const int& rx = x; // rx是const int& f(x); // 左值,T是int&,param是int& f(cx); // 左值,T是const int&,param是const int& f(rx); // 左值,T是const int&,param是const int& f(10); // 右值,T是int,而param是int&& 

因此,只要區分開左值與右值傳入,上面的類型推斷就清晰多了。

情形3:ParamType不是指針也不是引用類型

若是ParamType既不是引用類型,也不是指針類型,那就意味着函數的參數是傳值了:

template <typename T> void f(T param); // 此時param是傳值方式 

傳值方式意味着param是傳入對象的一個新副本,相應地,類型推斷規則爲:

  1. 若是expr類型是引用,那麼其引用屬性被忽略;
  2. 若是忽略了expr的引用特性後,其是const類型,那麼也忽略掉。

下面是例子:

int x = 10; // x是int const int cx = x; // cx是const int const int& rx = x; // rx是const int& f(x); // T和param都是int f(cx); // T和param仍是int f(rx); // T和param還是int 

其實上面的規則不難理解,由於param是一個新對象,不論其如何改變,都不會影響傳入的參數,因此引用屬性與const屬性都被忽略了。可是有個特殊的狀況,當你送入指針變量時,會有些變化:

const char* const ptr = "Hello, world"; // ptr是一個指向常量的常量指針 f(ptr); 

儘管仍是傳值方式,可是複製是指針,固然改變指針自己的值不會影響傳入的指針值,因此指針的const屬性能夠被忽略。可是指針指向常量的屬性卻不能忽略,由於你能夠經過指針的副本解引用,而後就修改了指針所指向的值,原來的指針指向的內容也會跟着變化,可是原來的指針指向的是const對象。矛盾會產生,因此這個屬性沒法忽略。所以,ptr的類型是const char*

儘管前面三種狀況已經包含了可能,可是對於特定函數參數,仍然會有特殊狀況。第一狀況是傳入的參數是數組,咱們知道若是函數參數是數組,其是當作指針來處理的,因此下面的兩個函數聲明是等價的:

void fun(int arr[]); // 數組形式 void fun(int* arr); // 指針形式 // 二者是等價的 

因此,對於函數模板類型推斷來講,數組參數推斷的也是指針類型,好比傳值方式:

template <typename T> void f(T param); // 傳值方式 const char[] name = "Julie"; // name是char[6]數組 f(name); // 此時T和param是const char*類型 

可是若是是引用方式,事情就發生了變化,此時數組再也不被當作指針類型,而就是固定長度的數組。因此:

template <typename T> void f(T& param); // 引用類型 const char[] name = "Julie"; // name是char[6]數組 f(name); // 此時T是const char[6],而param類型是const char (&)[6] 

顯然與傳值方式不一樣,很難讓人理解,可是事實就是如此。可是這也暴漏了一個事實:數組的引用利用函數模板能夠推導出數組的大小,下面是一個能夠返回數組大小的函數實現:

template <typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) noexcept { // 因爲並不實際須要數組,只用到其類型推斷,因此不須要參數 return N; } int arr[] = {1, 3, 7, 2, 9}; const int size = arraySize(arr); // 5 

真實很神奇的一個函數,可是一切又合情合理!

另一個特殊狀況就是傳遞的參數是函數,其實也是當作指針,和數組參數相似:

template <typename T> void f1(T param); // 傳值方式 template <typename T> void f2(T& param); // 引用方式 void someFun(int); // 類型爲void (int) f1(someFun); // T和param是 void (*) (int)類型 f2(someFun); // T是void (int)(不是指針類型),但param是void (&) (int)類型 // 儘管如此,實際使用時差異不大,用於回調函數時,通常不會去修改那個函數吧 

auto類型推斷

C++11引入了auto關鍵字,用於變量定義時的類型自動推斷。從表面上看,auto與模板類型推斷的做用對象是不同的。可是二者其實是一致的,函數模板推斷的任務是:

template <typename T> void f(ParamType param); f(expr); // 根據expr類型推導出T和ParamType的類型 

編譯器要根據expr類型推導出T和ParamType的類型。移植到auto上是那麼容易:把auto當作函數模板中的T,而把變量的實際類型當作ParamType。這樣咱們能夠把auto類型推斷轉換成函數模板類型推斷,仍是例子說話:

// auto推斷例子 auto x = 10; const auto cx = x; const auto& rx = x; // 傳化爲模板類型推斷 template <typename T> void f1(T param); f1(10); template <typename T> void f2(const T param); f2(x); template <typename T> void f3(const T& param); f3(x); 

顯然,很容易推斷出各個變量的類型。前面說到,函數模板類型推斷有三種狀況,那麼對於auto來講,仍然有三種情形:

  1. 類型修飾符是一個指針或者引用,可是不是通用引用;
  2. 類型修飾符是一個通用引用;
  3. 類型修飾符不是指針,也不是引用。

下面是具體例子:

const int N = 2; auto x = 10; // 情形3: int const auto cx = x; // 情形3: const int const auto& rx = x; // 情形1:const int& auto y = N; // 情形3: int // 情形2 auto&& y1 = x; // 左值:int& auto&& y2 = cx; // 左值: const int& auto&& y3 = 10; // 右值:int&& 

能夠看到,auto與函數模板類型推斷本質上是一致的。可是有一個特殊狀況,那就是C++11支持統一初始化方式:

// 等價的初始化方式 int x1 = 9; int x2(9); // 統一初始化 int x3 = {9}; int x4{9}; 

上面的4種方式均可以用來初始化一個值爲9的int變量,那麼你可能會想下面的代碼是一樣的效果:

auto x1 = 9; auto x2(9); auto x3 = {9}; auto x4{9}; 

可是實際上不是這樣:對於前兩個,確實是初始化了值爲9的int類型變量,可是後二者確是獲得了包含元素9的std::initialzer_list<int>對象(初始化列表),這算是auto的一個特例吧。可是這對函數模板類型推斷並不適用:

auto x = {1, 3, 5} // 合法:std::initializer_list<int>類型 template<typename T> void f(T param); f({1, 3, 5}); // 非法,沒法編譯:不能推斷出T的類型 // 能夠修改爲下面 template <typename T> void f2(std::initializer_list<T> param); f2({1, 3, 5}); // 合法:T是int,param是std::initializer_list<int> 

上面講的都是關於auto用於變量定義時的類型推斷。可是C++14auto還能夠用於函數返回類型的推斷以及泛型lambda表達式(其參數支持自動推斷類型)。以下面的例子:

// C++14功能 // 定義一個判斷是否大於10的泛型lambda表達式 auto isGreaterThan10 = [] (auto i) { return i > 10;}; bool r = isisGreaterThan10(20); // false // auto用於函數返回類型自動推斷 auto multiplyBy2Lambda(int x) { return [x] {return 2 * x;}; } auto f = multiplyBy2Lambda(4); cout << f() << endl; // 8 

這些例子是auto用於模板類型推斷,不一樣於前面的定義變量時的類型推斷,不能使用初始化列表來推斷:

// 如下都是沒法編譯的 auto createList() { return {1, 3, 5}; } auto f = [](auto v) {}; f({1, 3, 5}); 

總之,auto與模板類型推斷是一致的,除了要注意初始化列表這種特殊狀況。

decltype關鍵字

decltype用於返回某一實體(變量名與表達式)的類型。咱們從最簡單的例子開始:

const int x = 0; // decltype(x)是const int struct Point {int x; int y;}; Point p{2, 5}; // decltype(Point::x)是int; decltype(p.x)是int bool f(int x); // decltype(f)是bool(int) // decltype(f(2.0))是bool vector<int> v{2, 5}; // decltype(v)是vector<int> // decltype(v[0])是int& 

大部分狀況,decltype按照你所預料的方式工做:decltype用於一個變量名時,返回的正是該變量所對應的類型;用於函數返回值也正是函數返回值類型。可是當用於左值表達式時,decltype推斷出的類型卻必定是一個引用類型,看下面的例子:

int x = 10; // decltype(x)是int,可是decltype((x))確是int& struct A {double x;}; const A* a = new A{2.0}; // decltype(a->x)是double,可是decltype((a->x))確是const double& 

讓人感受很是奇怪。其實普遍的C++表達式(字面值,變量名,表達式等等)包含兩個獨立的屬性:類型(type)和值種類(value category)。這裏的類型指的是非引用類型,而值種類有三個基本類型:xvalue,lvalueprvalue。當decltype做用於不一樣值種類的表達式上,其效果不同。具體能夠參考這裏(反正有點複雜)。

上面的簡單瞭解就好,由於用的並非太多。而decltype的一個很重要的應用是在函數模板中的返回值類型推斷。這裏舉個例子:你想寫一個函數,這個函數接收兩個參數,一個支持索引操做符的容器對象,一個是索引參數;函數驗證用戶身份,而後返回值這個容器對象在該索引值處的元素,要求其返回類型與容器對象索引操做返回值類型同樣。此時就可使用decltype,先看一下下面的實現:

// C++11 template <typename Container, typename Index> auto authAndAccesss(Container& c, Index i) ->decltype(c[i]) { // 驗證用戶 // ... return c[i]; } 

這種實現使用了C++11中的「拖尾返回類型」:函數返回類型要在參數列表以後聲明(使用->分割),使用「拖尾返回類型」,咱們能夠利用函數的參數來推斷返回類型:上面就用了c[i]來推斷返回值類型。還有注意的是上面的auto沒有推斷功能,僅僅是指明使用了「拖尾返回類型」。你們可能會想,爲何不把decltype(c[i])直接替換auto的位置?這樣是不行的,由於此時函數參數尚未被建立!

可是C++14容許你省略掉拖尾部分:

// C++14 template <typename Container, typename Index> auto authAndAccesss(Container& c, Index i) { // 驗證用戶 // ... return c[i]; } 

此時僅留下auto,此時auto真正用於返回值類型推斷:即根據返回值表達式c[i]來推斷返回類型。此時,問題來了。咱們知道容器的索引操做返回的大部分是引用類型,可是auto推導類型時,會忽略c[i]的引用屬性,那麼函數返回值是一個右值(儘管咱們但願它仍然是左值),下面的代碼就存在問題:

vector<int> v{1, 2, 3, 4, 5}; authAndAccess(v, 2) = 10; // 沒法編譯:沒法對右值賦值 

咱們知道decltype(c[i])是能夠正常推斷的,因此,爲了解決上面的問題,C++14引入了decltype(auto)標識符:auto說明類型須要推斷,decltype說明類型推斷要使用decltype規則。因此,再次修改代碼:

template <typename Container, typename Index> decltype(auto) authAndAccesss(Container& c, Index i) { // 驗證用戶 // ... return c[i]; } 

此時,若是c[i]的返回類型是引用類型,那麼函數的返回類型也是引用類型。其實decltype(auto)還能夠用於聲明變量:

int x = 10; const int& cx = x; auto y = cw; // 類型是int decltype(auto) z = cw; // 類型是const int& 

對於修改版本的authAndAccesss,一個問題你只能傳遞左值引用的容器對象,而且該對象不能是常量左值引用。可是咱們想既能夠傳遞左值又能夠傳遞右值,這個時候你須要使用&&通用引用:

template <typename Container, typename Index> decltype(auto) authAndAccesss(Container&& c, Index i) { // 驗證用戶 // ... return std::forward(c)[i]; } 

其中std::forward函數是專門處理通用引用類型參數的,基本上就是傳入的參數是右值,轉化的仍是右值引用,若是是左值,那麼轉化的是左值引用,具體能夠參考這裏

相關文章
相關標籤/搜索