C++類型推斷

C++類型推斷

對(duì)于靜態(tài)語言來說,你一般要明確告訴編譯器變量或者表達(dá)式的類型。但是慶幸地是,現(xiàn)在C++已經(jīng)引入了自動(dòng)類型推斷:編譯器可以自動(dòng)推斷出類型。在C++11之前,類型推斷只是用在模板上。而C++11通過引入兩個(gè)關(guān)鍵字autodecltype擴(kuò)展了類型推斷的應(yīng)用。C++14更進(jìn)一步擴(kuò)展了autodecltype的應(yīng)用范圍。明顯地,類型推斷可以減少很多無必要的工作。但是高興之余,你仍然有可能會(huì)犯一些錯(cuò)誤,如果你不能深入理解類型推斷背后的規(guī)則與機(jī)理。因此,我們分別從模板類型推斷、autodecltype的使用三個(gè)方面深入講解類型推斷。

模板類型推斷

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

template <typename T>
void f(ParamType param);

f(expr);   // 對(duì)函數(shù)進(jìn)行調(diào)用

編譯器要根據(jù)expr來推斷出TParamType的類型。特別注意的是,這兩個(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)是:

  1. 如果expr是引用類型,那就忽略引用部分;
  2. 通過相減exprParamType的類型來決定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ī)則如下:

  1. 如果expr是左值,TParamType都推導(dǎo)為左值引用,盡管其形式上是右值引用(此時(shí)僅把&&匹配符,一旦匹配是左值引用,那么&&可以忽略了)。
  2. 如果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ī)則為:

  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

其實(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來說,仍然有三種情形:

  1. 類型修飾符是一個(gè)指針或者引用,但是不是通用引用;
  2. 類型修飾符是一個(gè)通用引用;
  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與函數(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++14auto還可以用于函數(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,lvalueprvalue。當(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)然加入了自己的理解,有任何問題可以參考原書。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評(píng)論 6 529
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,916評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,014評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評(píng)論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,245評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,795評(píng)論 1 320
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,010評(píng)論 0 285
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,524評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,487評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,634評(píng)論 1 366
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,884評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,236評(píng)論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,623評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容