C++類型推斷
對(duì)于靜態(tài)語言來說,你一般要明確告訴編譯器變量或者表達(dá)式的類型。但是慶幸地是,現(xiàn)在C++已經(jīng)引入了自動(dòng)類型推斷:編譯器可以自動(dòng)推斷出類型。在C++11
之前,類型推斷只是用在模板上。而C++11
通過引入兩個(gè)關(guān)鍵字auto
和decltype
擴(kuò)展了類型推斷的應(yīng)用。C++14
更進(jìn)一步擴(kuò)展了auto
和decltype
的應(yīng)用范圍。明顯地,類型推斷可以減少很多無必要的工作。但是高興之余,你仍然有可能會(huì)犯一些錯(cuò)誤,如果你不能深入理解類型推斷背后的規(guī)則與機(jī)理。因此,我們分別從模板類型推斷、auto
和decltype
的使用三個(gè)方面深入講解類型推斷。
模板類型推斷
模板類型推斷在C++98
中就已經(jīng)引入了,它也是理解auto
與decltype
的基石。下面是一個(gè)函數(shù)模板的通用例子:
template <typename T>
void f(ParamType param);
f(expr); // 對(duì)函數(shù)進(jìn)行調(diào)用
編譯器要根據(jù)expr
來推斷出T
與ParamType
的類型。特別注意的是,這兩個(gè)類型有可能并不相同,因?yàn)?code>ParamType可能會(huì)包含修飾詞,比如const
和&
。看下面的例子:
template <typename T>
void f(const T& param);
int x = 0;
f(x); // 使用int類型調(diào)用函數(shù)
此時(shí)類型推斷結(jié)果是:T
的類型是int
,但是ParamType
的類型卻是const int&
。所以,兩個(gè)類型并不相同。還有,你可能很自然地認(rèn)為T
的類型與表達(dá)式expr
是一樣的,比如上面的例子:兩者是一樣的。但是實(shí)際上這也是誤區(qū):T
的類型不僅取決于expr
,也與ParamType
緊緊相關(guān)。這存在三種不同的情形:
情形1:ParamType是指針或者引用類型
最簡單的情況ParamType
是指針或者引用類型,但不是通用引用類型(&&)。此時(shí),類型推斷要點(diǎn)是:
- 如果
expr
是引用類型,那就忽略引用部分; - 通過相減
expr
與ParamType
的類型來決定T
的類型。
比如,下面是引用類型的例子:
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); // 此時(shí)T為int,而param是int&
f(cx); // 此時(shí)T為const int,而param是const int&
f(rx); // 此時(shí)T為const int,而param是const int&
其中可以看到,const對(duì)象傳遞給接收T&
參數(shù)的函數(shù)模板時(shí),const屬性是能夠被T
所捕獲的,即const稱為T
的一部分。同時(shí),引用類型對(duì)象的引用屬性是可以忽略的,并沒有被T
所捕獲。上面處理的其實(shí)是左值引用,對(duì)于右值引用,規(guī)則是相同的,但是右值引用的通配符T&&
還有另外的含義,會(huì)在后面講。
如果param
是常量引用類型,推斷也是相似的,盡管有些區(qū)別:
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); // 此時(shí)T為int,而param是const int&
f(cx); // 此時(shí)T為int,而param是const int&
f(rx); // 此時(shí)T為int,而param是const int&
指針類型也同樣適用:
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); // 此時(shí)T是int,而param是int*
f(cpx); // 此時(shí)T是const int,而param是const int*
顯然,這種情形類型推斷很容易。
情形2:ParamType是通用引用類型(&&)
這種情形有點(diǎn)復(fù)雜,因?yàn)橥ㄓ靡妙愋蛥?shù)與右值引用參數(shù)的形式是一樣的,但是它們是有區(qū)別的,前者允許左值傳入。類型推斷的規(guī)則如下:
- 如果
expr
是左值,T
和ParamType
都推導(dǎo)為左值引用,盡管其形式上是右值引用(此時(shí)僅把&&匹配符,一旦匹配是左值引用,那么&&可以忽略了)。 - 如果
expr
是右值,可以看成情形1的右值引用。
規(guī)則有點(diǎn)繞,還是例子說話:
template <typename T>
void f(T&& param); // 此時(shí)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&&
所以,只要區(qū)分開左值與右值傳入,上面的類型推斷就清晰多了。
情形3:ParamType不是指針也不是引用類型
如果ParamType
既不是引用類型,也不是指針類型,那就意味著函數(shù)的參數(shù)是傳值了:
template <typename T>
void f(T param); // 此時(shí)param是傳值方式
傳值方式意味著param
是傳入對(duì)象的一個(gè)新副本,相應(yīng)地,類型推斷規(guī)則為:
- 如果
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
其實(shí)上面的規(guī)則不難理解,因?yàn)?code>param是一個(gè)新對(duì)象,不論其如何改變,都不會(huì)影響傳入的參數(shù),所以引用屬性與const屬性都被忽略了。但是有個(gè)特殊的情況,當(dāng)你送入指針變量時(shí),會(huì)有些變化:
const char* const ptr = "Hello, world"; // ptr是一個(gè)指向常量的常量指針
f(ptr);
盡管還是傳值方式,但是復(fù)制是指針,當(dāng)然改變指針本身的值不會(huì)影響傳入的指針值,所以指針的const屬性可以被忽略。但是指針指向常量的屬性卻不能忽略,因?yàn)槟憧梢酝ㄟ^指針的副本解引用,然后就修改了指針?biāo)赶虻闹担瓉淼闹羔樦赶虻膬?nèi)容也會(huì)跟著變化,但是原來的指針指向的是const對(duì)象。矛盾會(huì)產(chǎn)生,所以這個(gè)屬性無法忽略。因此,ptr的類型是const char*
。
盡管前面三種情況已經(jīng)包含了可能,但是對(duì)于特定函數(shù)參數(shù),仍然會(huì)有特殊情況。第一情況是傳入的參數(shù)是數(shù)組,我們知道如果函數(shù)參數(shù)是數(shù)組,其是當(dāng)做指針來處理的,所以下面的兩個(gè)函數(shù)聲明是等價(jià)的:
void fun(int arr[]); // 數(shù)組形式
void fun(int* arr); // 指針形式
// 兩者是等價(jià)的
所以,對(duì)于函數(shù)模板類型推斷來說,數(shù)組參數(shù)推斷的也是指針類型,比如傳值方式:
template <typename T>
void f(T param); // 傳值方式
const char[] name = "Julie"; // name是char[6]數(shù)組
f(name); // 此時(shí)T和param是const char*類型
但是如果是引用方式,事情就發(fā)生了變化,此時(shí)數(shù)組不再被當(dāng)做指針類型,而就是固定長度的數(shù)組。所以:
template <typename T>
void f(T& param); // 引用類型
const char[] name = "Julie"; // name是char[6]數(shù)組
f(name); // 此時(shí)T是const char[6],而param類型是const char (&)[6]
顯然與傳值方式不同,很難讓人理解,但是事實(shí)就是如此。但是這也暴漏了一個(gè)事實(shí):數(shù)組的引用利用函數(shù)模板可以推導(dǎo)出數(shù)組的大小,下面是一個(gè)可以返回?cái)?shù)組大小的函數(shù)實(shí)現(xiàn):
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
// 由于并不實(shí)際需要數(shù)組,只用到其類型推斷,所以不需要參數(shù)
return N;
}
int arr[] = {1, 3, 7, 2, 9};
const int size = arraySize(arr); // 5
真實(shí)很神奇的一個(gè)函數(shù),但是一切又合情合理!
另外一個(gè)特殊情況就是傳遞的參數(shù)是函數(shù),其實(shí)也是當(dāng)做指針,和數(shù)組參數(shù)類似:
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)類型
// 盡管如此,實(shí)際使用時(shí)差別不大,用于回調(diào)函數(shù)時(shí),一般不會(huì)去修改那個(gè)函數(shù)吧
auto類型推斷
C++11
引入了auto
關(guān)鍵字,用于變量定義時(shí)的類型自動(dòng)推斷。從表面上看,auto
與模板類型推斷的作用對(duì)象是不一樣的。但是兩者實(shí)際上是一致的,函數(shù)模板推斷的任務(wù)是:
template <typename T>
void f(ParamType param);
f(expr); // 根據(jù)expr類型推導(dǎo)出T和ParamType的類型
編譯器要根據(jù)expr類型推導(dǎo)出T和ParamType的類型。移植到auto
上是那么容易:把auto
看成函數(shù)模板中的T,而把變量的實(shí)際類型看成ParamType。這樣我們可以把auto
類型推斷轉(zhuǎn)換成函數(shù)模板類型推斷,還是例子說話:
// 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);
顯然,很容易推斷出各個(gè)變量的類型。前面說到,函數(shù)模板類型推斷有三種情況,那么對(duì)于auto
來說,仍然有三種情形:
- 類型修飾符是一個(gè)指針或者引用,但是不是通用引用;
- 類型修飾符是一個(gè)通用引用;
- 類型修飾符不是指針,也不是引用。
下面是具體例子:
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
與函數(shù)模板類型推斷本質(zhì)上是一致的。但是有一個(gè)特殊情況,那就是C++11
支持統(tǒng)一初始化方式:
// 等價(jià)的初始化方式
int x1 = 9;
int x2(9);
// 統(tǒng)一初始化
int x3 = {9};
int x4{9};
上面的4種方式都可以用來初始化一個(gè)值為9的int變量,那么你可能會(huì)想下面的代碼是同樣的效果:
auto x1 = 9;
auto x2(9);
auto x3 = {9};
auto x4{9};
但是實(shí)際上不是這樣:對(duì)于前兩個(gè),確實(shí)是初始化了值為9的int類型變量,但是后兩者確是得到了包含元素9的std::initialzer_list<int>
對(duì)象(初始化列表),這算是auto
的一個(gè)特例吧。但是這對(duì)函數(shù)模板類型推斷并不適用:
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>
上面講的都是關(guān)于auto
用于變量定義時(shí)的類型推斷。但是C++14
中auto
還可以用于函數(shù)返回類型的推斷以及泛型lambda
表達(dá)式(其參數(shù)支持自動(dòng)推斷類型)。如下面的例子:
// C++14功能
// 定義一個(gè)判斷是否大于10的泛型lambda表達(dá)式
auto isGreaterThan10 = [] (auto i) { return i > 10;};
bool r = isisGreaterThan10(20); // false
// auto用于函數(shù)返回類型自動(dòng)推斷
auto multiplyBy2Lambda(int x)
{
return [x] {return 2 * x;};
}
auto f = multiplyBy2Lambda(4);
cout << f() << endl; // 8
這些例子是auto
用于模板類型推斷,不同于前面的定義變量時(shí)的類型推斷,不能使用初始化列表來推斷:
// 以下都是無法編譯的
auto createList()
{
return {1, 3, 5};
}
auto f = [](auto v) {};
f({1, 3, 5});
總之,auto
與模板類型推斷是一致的,除了要注意初始化列表這種特殊情況。
decltype關(guān)鍵字
decltype
用于返回某一實(shí)體(變量名與表達(dá)式)的類型。我們從最簡單的例子開始:
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
按照你所預(yù)料的方式工作:decltype
用于一個(gè)變量名時(shí),返回的正是該變量所對(duì)應(yīng)的類型;用于函數(shù)返回值也正是函數(shù)返回值類型。但是當(dāng)用于左值表達(dá)式時(shí),decltype
推斷出的類型卻一定是一個(gè)引用類型,看下面的例子:
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&
讓人感覺非常奇怪。其實(shí)廣泛的C++表達(dá)式(字面值,變量名,表達(dá)式等等)包含兩個(gè)獨(dú)立的屬性:類型(type)和值種類(value category)。這里的類型指的是非引用類型,而值種類有三個(gè)基本類型:xvalue
,lvalue
和prvalue
。當(dāng)decltype
作用于不同值種類的表達(dá)式上,其效果不一樣。具體可以參考這里(反正有點(diǎn)復(fù)雜)。
上面的簡單了解就好,因?yàn)橛玫牟⒉皇翘唷6?code>decltype的一個(gè)很重要的應(yīng)用是在函數(shù)模板中的返回值類型推斷。這里舉個(gè)例子:你想寫一個(gè)函數(shù),這個(gè)函數(shù)接收兩個(gè)參數(shù),一個(gè)支持索引操作符的容器對(duì)象,一個(gè)是索引參數(shù);函數(shù)驗(yàn)證用戶身份,然后返回值這個(gè)容器對(duì)象在該索引值處的元素,要求其返回類型與容器對(duì)象索引操作返回值類型一樣。此時(shí)就可以使用decltype
,先看一下下面的實(shí)現(xiàn):
// C++11
template <typename Container, typename Index>
auto authAndAccesss(Container& c, Index i)
->decltype(c[i])
{
// 驗(yàn)證用戶
// ...
return c[i];
}
這種實(shí)現(xiàn)使用了C++11
中的“拖尾返回類型”:函數(shù)返回類型要在參數(shù)列表之后聲明(使用->分割),使用“拖尾返回類型”,我們可以利用函數(shù)的參數(shù)來推斷返回類型:上面就用了c[i]
來推斷返回值類型。還有注意的是上面的auto
沒有推斷功能,僅僅是指明使用了“拖尾返回類型”。大家可能會(huì)想,為什么不把decltype(c[i])
直接替換auto
的位置?這樣是不行的,因?yàn)榇藭r(shí)函數(shù)參數(shù)還沒有被創(chuàng)建!
但是C++14
允許你省略掉拖尾部分:
// C++14
template <typename Container, typename Index>
auto authAndAccesss(Container& c, Index i)
{
// 驗(yàn)證用戶
// ...
return c[i];
}
此時(shí)僅留下auto
,此時(shí)auto
真正用于返回值類型推斷:即根據(jù)返回值表達(dá)式c[i]
來推斷返回類型。此時(shí),問題來了。我們知道容器的索引操作返回的大部分是引用類型,但是auto
推導(dǎo)類型時(shí),會(huì)忽略c[i]
的引用屬性,那么函數(shù)返回值是一個(gè)右值(盡管我們希望它仍然是左值),下面的代碼就存在問題:
vector<int> v{1, 2, 3, 4, 5};
authAndAccess(v, 2) = 10; // 無法編譯:無法對(duì)右值賦值
我們知道decltype(c[i])
是可以正常推斷的,所以,為了解決上面的問題,C++14
引入了decltype(auto)
標(biāo)識(shí)符:auto
說明類型需要推斷,decltype
說明類型推斷要使用decltype
規(guī)則。所以,再次修改代碼:
template <typename Container, typename Index>
decltype(auto) authAndAccesss(Container& c, Index i)
{
// 驗(yàn)證用戶
// ...
return c[i];
}
此時(shí),如果c[i]
的返回類型是引用類型,那么函數(shù)的返回類型也是引用類型。其實(shí)decltype(auto)
還可以用于聲明變量:
int x = 10;
const int& cx = x;
auto y = cw; // 類型是int
decltype(auto) z = cw; // 類型是const int&
對(duì)于修改版本的authAndAccesss,一個(gè)問題你只能傳遞左值引用的容器對(duì)象,并且該對(duì)象不能是常量左值引用。但是我們想既可以傳遞左值又可以傳遞右值,這個(gè)時(shí)候你需要使用&&
通用引用:
template <typename Container, typename Index>
decltype(auto) authAndAccesss(Container&& c, Index i)
{
// 驗(yàn)證用戶
// ...
return std::forward(c)[i];
}
其中std::forward
函數(shù)是專門處理通用引用類型參數(shù)的,基本上就是傳入的參數(shù)是右值,轉(zhuǎn)化的還是右值引用,如果是左值,那么轉(zhuǎn)化的是左值引用,具體可以參考這里。
終于完了,本教程算是《Effective Modern C++》第一章的學(xué)習(xí)筆記,當(dāng)然加入了自己的理解,有任何問題可以參考原書。