閱讀本文需要具有的預備知識:

左值和右值的基本概念

模板推導的基本規則

若無特殊說明,本文中的大寫字母

T

泛指任意的資料型別

引用摺疊

我們把

引用摺疊

拆解為

引用

摺疊

兩個短語來解釋。

首先,

引用

的意思眾所周知,當我們使用某個物件的別名的時候就好像直接使用了該物件,這也就是引用的含義。在C++11中,新加入了右值的概念。所以引用的型別就有兩種形式:左值引用

T&

和右值引用

T&&

其次,解釋一下摺疊的含義。所謂的摺疊,就是多個的意思。上面介紹引用分為左值引用和右值引用兩種,那麼將這兩種型別進行排列組合,就有四種情況:

-

左值

-

左值

T

&

&

-

左值

-

右值

T

&

&&

-

右值

-

左值

T

&&

&

-

右值

-

右值

T

&&

&&

這就是所謂的引用摺疊!引用摺疊的含義到這裡就結束了。

但是

,當我們在IDE中敲下類似這樣的程式碼:

// 。。。

int a = 0;

int &ra = a;

int & &rra = ra; // 編譯器報錯:不允許使用引用的引用!

// 。。。

WTF ! 既然不允許使用,為啥還要有引用摺疊這樣的概念存在 ?!

原因就是:引用摺疊的應用場景不在這裡!!

下面我們介紹引用摺疊在模板中的應用:

完美轉發

。在介紹完美轉發之前,我們先介紹一下

萬能引用

萬能引用

所謂的

萬能引用

並不是C++的語法特性,而是我們利用現有的C++語法,自己實現的一個功能。因為這個功能既能接受左值型別的引數,也能接受右值型別的引數。所以叫做萬能引用。

萬能引用的形式如下:

template

<

typename

T

>

ReturnType

Function

T

&&

parem

{

// 函式功能實現

}

接下來,我們看一下為什麼上面這個函式能

萬能引用

不同型別的引數。

為了更加直觀的看到效果,我們藉助

Boost

庫的部分功能,重寫我們的萬能引用函式:

如果不瞭解Boost庫也沒關係,Boost庫主要是為了幫助大家看到模板裡引數型別)

#include

#include

using namespace std;

using boost::typeindex::type_id_with_cvr;

template

void PrintType(T&& param)

{

// 利用Boost庫列印模板推匯出來的 T 型別

cout << “T type:” << type_id_with_cvr()。pretty_name() << endl;

// 利用Boost庫列印形參的型別

cout << “param type:” << type_id_with_cvr()。pretty_name() << endl;

}

int main(int argc, char *argv[])

{

int a = 0; // 左值

PrintType(a); // 傳入左值

int &lvalue_refence_a = a; // 左值引用

PrintType(lvalue_refence_a); // 傳入左值引用

PrintType(int(2)); // 傳入右值

}

透過上面的程式碼可以清楚的看到,

void PrintType(T&& param)

可以接受任何型別的引數。嗯,真的是萬能引用!

到這裡的話,萬能引用的介紹也就結束了。

但是我們只看到了這個東西可以接受任何的引數,卻不知道為什麼它能這麼做。

下面,我們來仔細觀察並分析一下

main

函式中對

PrintType()

的各個呼叫結果。

傳入左值

int a = 0; // 左值

PrintType(a); // 傳入左值

/***************************************************/

輸出:T type : int &

param type : int &

我們將T的推導型別

int&

帶入模板,得到例項化的型別:

void PrintType(int& && param)

{

// 。。。

}

重點來了!編譯器將T推導為 int& 型別。當我們用 int& 替換掉 T 後,得到 int & &&。

MD,編譯器不允許我們自己把程式碼寫成int& &&,它自己卻這麼幹了 =。=

那麼 int & &&到底是個什麼東西呢?!(它是引用摺疊,剛開始就說了啊 =。=)

下面,就是

引用摺疊的精髓了

所有的引用摺疊最終都代表一個引用,要麼是左值引用,要麼是右值引用。

規則就是:

如果任一引用為左值引用,則結果為左值引用。否則(即兩個都是右值引用),結果為右值引用。

《Effective Modern C++》

也就是說,

int& &&

等價於

int &

void PrintType(int& && param)

==

void PrintType(int& param)

所以傳入右值之後,函式模板推導的最終版本就是:

void PrintType(int& param)

{

// 。。。

}

所以,它能接受一個左值

a

現在我們重新整理一下思路:

編譯器不允許我們寫下類似

int & &&

這樣的程式碼,但是它自己卻可以推匯出

int & &&

程式碼出來。它的理由就是:我(編譯器)雖然推匯出

T

int&

,但是我在最終生成的程式碼中,

利用引用摺疊規則

,將

int & &&

等價生成了

int &

。推匯出來的

int & &&

只是過渡階段,最終版本並不存在。所以也不算破壞規定咯。

關於有的人會問,我傳入的是一個左值a,並不是一個左值引用,為什麼編譯器會推匯出T 為int &呢。

首先,模板函式引數為 T&& param,也就是說,不管T是什麼型別,T&&的最終結果必然是一個引用型別。如果T是int, 那麼T&& 就是 int &&;如果T為 int &,那麼 T &&(int& &&) 就是&,如果T為&&,那麼T &&(&& &&) 就是&&。很明顯,接受左值的話,T只能推導為int &。

2。明白傳入左值的推導結果,剩下的幾個呼叫結果就很明顯了:

int &lvalue_refence_a = a; //左值引用

PrintType(lvalue_refence_a); // 傳入左值引用

/*

* T type : int &

* T && : int & &&

* param type : int &

*/

PrintType(int(2)); // 傳入右值

/*

* T type : int

* T && : int &&

* param type : int &&

*/

以上就是萬能引用的全部了。總結一下,萬能引用就是利用模板推導和引用摺疊的相關規則,生成不同的例項化模板來接收傳進來的引數。

完美轉發

好了,有了萬能引用。當我們既需要接收左值型別,又需要接收右值型別的時候,再也不用分開寫兩個過載函數了。那麼,什麼情況下,我們需要一個函式,既能接收左值,又能接收右值呢?

答案就是:轉發的時候。

於是,我們馬上想到了萬能引用。又於是興沖沖的改寫了以上的程式碼如下:

/*

* Boost庫在這裡已經不需要了,我們將其拿掉,可以更簡潔的看清楚轉發的程式碼實現

*/

#include

using namespace std;

// 萬能引用,轉發接收到的引數 param

template

void PrintType(T&& param)

{

f(param); // 將引數param轉發給函式 void f()

}

// 接收左值的函式 f()

template

void f(T &)

{

cout << “f(T &)” << endl;

}

// 接收右值的函式f()

template

void f(T &&)

{

cout << “f(T &&)” << endl;

}

int main(int argc, char *argv[])

{

int a = 0;

PrintType(a);//傳入左值

PrintType(int(0));//傳入右值

}

我們執行上面的程式碼,按照預想,在main中我們給 PrintType 分別傳入一個左值和一個右值。PrintType將引數轉發給 f() 函式。f()有兩個過載,分別接收左值和右值。

正常的情況下,

PrintType(a);

應該列印

f(T&)

PrintType(int());

應該列印

f(T&&)

但是

,真實的輸出結果是

f(T &);

f(T &);

為什麼明明傳入了不同型別的值,但是

void f()

函式只調用了

void f(int &)

的版本。這說明,不管我們傳入的引數型別是什麼,在

void PrintType(T&& param)

函式的內部,

param

都是一個左值引用!

沒錯,事實就是這樣。當外部傳入引數給 PrintType 函式時,param既可以被初始化為左值引用,也可以被初始化為右值引用,取決於我們傳遞給 PrintType 函式的實參型別。但是,當我們在函式 PrintType 內部,將param傳遞給另一個函式的時候,此時,param是被當作左值進行傳遞的。

應為這裡的 param 是個具名的物件。我們不進行詳細的探討了。大家只需要己住,任何的函式內部,對形參的直接使用,都是按照左值進行的。

WTF,萬能引用內部形參都變成了左值!那我還要什麼萬能引用啊!直接改為左值引用不就好了!!

別急,我們可以透過一些其它的手段改變這個情況,比如使用 std::forward 。

在萬能引用的一節,我們應該有所感覺了。使用萬能引用的時候,如果傳入的實參是個右值(包括右值引用),那麼,模板型別 T 被推導為 實參的型別(沒有引用屬性),如果傳入實參是個左值,T被推導為左值引用。

也就是說,模板中的 T 儲存著傳遞進來的實參的資訊,我們可以利用 T 的資訊來強制型別轉換我們的 param 使它和實參的型別一致。

具體的做法就是,將模板函式

void PrintType(T&& param)

中對

f(param)

的呼叫,改為

f(std::forward(param));

然後重新執行一下程式。輸出如下:

f(T &);

f(T &&);

嗯,完美的轉發!

那麼,

std::forward

是怎麼利用到 T 的資訊的呢。

std::forward

的原始碼形式大致是這樣:

/*

* 精簡了標準庫的程式碼,在細節上可能不完全正確,但是足以讓我們瞭解轉發函式 forward 的了

*/

template

<

typename

T

>

T

&&

forward

T

&

param

{

return

static_cast

<

T

&&>

param

);

}

我們來仔細分析一下這段程式碼:

我們可以看到,不管T是值型別,還是左值引用,還是右值引用,T&經過引用摺疊,都將是左值引用型別。也就是forward 以左值引用的形式接收引數 param, 然後 透過將param進行強制型別轉換 static_cast<T&&> (),最終再以一個 T&&返回

所以,我們分析一下傳遞給 PrintType 的實參型別,並將推導的 T 型別代入 forward 就可以知道轉發的結果了。

1。傳入 PrintType 實參是右值型別:

根據以上的分析,可以知道T將被推導為值型別,也就是不帶有引用屬性,假設為 int 。那麼,將T = int 帶入forward。

int

&&

forward

int

&

param

{

return

static_cast

<

int

&&>

param

);

}

param

在forward內被強制型別轉換為 int &&

(static_cast(param))

, 然後按照int && 返回,兩個右值引用最終還是右值引用。最終保持了實參的右值屬性,轉發正確。

2。傳入 PrintType 實參是左值型別:

根據以上的分析,可以知道T將被推導為左值引用型別,假設為int&。那麼,將T = int& 帶入forward。

int& && forward(int& ¶m)

{

return static_cast(param);

}

引用摺疊一下就是:

int

&

forward

int

&

param

{

return

static_cast

<

int

&>

param

);

}

看到這裡,我想就不用再多說什麼了。傳遞給 PrintType 左值,forward返回一個左值引用,保留了實參的左值屬性,轉發正確。

到這裡,完美轉發也就介紹完畢了。

總結一下就是,透過引用摺疊,我們實現了萬能模板。在萬能模板內部,利用forward函式,本質上是又利用了一遍引用摺疊,實現了完美轉發。其中,模板推導扮演了至關重要的角色。