對於靜態語言來講,你通常要明確告訴編譯器變量或者表達式的類型。可是慶幸地是,如今C++已經引入了自動類型推斷:編譯器能夠自動推斷出類型。在C++11
以前,類型推斷只是用在模板上。而C++11
經過引入兩個關鍵字auto
和decltype
擴展了類型推斷的應用。C++14
更進一步擴展了auto
和decltype
的應用範圍。明顯地,類型推斷能夠減小不少無必要的工做。可是高興之餘,你仍然有可能會犯一些錯誤,若是你不能深刻理解類型推斷背後的規則與機理。所以,咱們分別從模板類型推斷、auto
和decltype
的使用三個方面深刻講解類型推斷。數組
模板類型推斷在C++98
中就已經引入了,它也是理解auto
與decltype
的基石。下面是一個函數模板的通用例子:函數
template <typename T> void f(ParamType param); f(expr); // 對函數進行調用
編譯器要根據expr
來推斷出T
與ParamType
的類型。特別注意的是,這兩個類型有可能並不相同,由於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
最簡單的狀況ParamType
是指針或者引用類型,但不是通用引用類型(&&)。此時,類型推斷要點是:指針
expr
是引用類型,那就忽略引用部分;expr
與ParamType
的類型來決定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
這種情形有點複雜,由於通用引用類型參數與右值引用參數的形式是同樣的,可是它們是有區別的,前者容許左值傳入。類型推斷的規則以下:
expr
是左值,T
和ParamType
都推導爲左值引用,儘管其形式上是右值引用(此時僅把&&匹配符,一旦匹配是左值引用,那麼&&能夠忽略了)。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&&
因此,只要區分開左值與右值傳入,上面的類型推斷就清晰多了。
若是ParamType
既不是引用類型,也不是指針類型,那就意味着函數的參數是傳值了:
template <typename T> void f(T param); // 此時param是傳值方式
傳值方式意味着param
是傳入對象的一個新副本,相應地,類型推斷規則爲:
expr
類型是引用,那麼其引用屬性被忽略;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)類型 // 儘管如此,實際使用時差異不大,用於回調函數時,通常不會去修改那個函數吧
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
來講,仍然有三種情形:
下面是具體例子:
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++14
中auto
還能夠用於函數返回類型的推斷以及泛型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
用於返回某一實體(變量名與表達式)的類型。咱們從最簡單的例子開始:
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
,lvalue
和prvalue
。當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
函數是專門處理通用引用類型參數的,基本上就是傳入的參數是右值,轉化的仍是右值引用,若是是左值,那麼轉化的是左值引用,具體能夠參考這裏。