一、理解引用折疊
(一)引用折疊
1. 在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意兩個&之間有空格)這種直接定義引用的引用是不合法的,但是編譯器在通過類型別名或模板參數推導等語境中,會間接定義出“引用的引用”,這時引用會形成“折疊”。
2. 引用折疊會發生在模板實例化、auto類型推導、創建和運用typedef和別名聲明、以及decltype語境中。
(二)引用折疊規則
1. 兩條規則
(1)所有右值引用折疊到右值引用上仍然是一個右值引用。如X&& &&折疊為X&&。
(2)所有的其他引用類型之間的折疊都將變成左值引用。如X& &, X& &&, X&& &折疊為X&。可見左值引用會傳染,沾上一個左值引用就變左值引用了。根本原因:在一處聲明為左值,就說明該對象為持久對象,編譯器就必須保證此對象可靠(左值)。
2. 利用引用折疊進行萬能引用初始化類型推導
(1)當萬能引用(T&& param)綁定到左值時,由於萬能引用也是一個引用,而左值只能綁定到左值引用。因此,T會被推導為T&類型。從而param的類型為T& &&,引用折疊后的類型為T&。
(2)當萬能引用(T&& param)綁定到右值時,同理,右值只能綁定到右值引用上,故T會被推導為T類型。從而param的類型就是T&&(右值引用)。
【編程實驗】引用折疊

#include <iostream> using namespace std; class Widget{}; template<typename T> void func(T&& param){} //Widget工廠函數 Widget widgetFactory() { return Widget(); } //類型別名 template<typename T> class Foo { public: typedef T&& RvalueRefToT; }; int main() { int x = 0; int& rx = x; //auto& & r = x; //error,聲明“引用的引用”是非法的! //1. 引用折疊發生的語境1——模板實例化 Widget w1; func(w1); //w1為左值,T被推導為Widget&。代入得void func(Widget& && param); //引用折疊后得void func(Widget& param) func(widgetFactory()); //傳入右值,T被推導為Widget,代入得void func(Widget&& param) //注意這里沒有發生引用的折疊。 //2. 引用折疊發生的語境2——auto類型推導 auto&& w2 = w1; //w1為左值auto被推導為Widget&,代入得Widget& && w2,折疊后為Widget& w2 auto&& w3 = widgetFactory(); //函數返回Widget,為右值,auto被推導為Widget,代入得Widget w3 //3. 引用折疊發生的語境3——tyedef和using Foo<int&> f1; //T被推導為int&,代入得typedef int& && RvalueRefToT;折疊后為typedef int& RvalueRefToT //4. 引用折疊發生的語境3——decltype decltype(x)&& var1 = 10; //由於x為int類型,代入得int&& rx。 decltype(rx) && var2 = x; //由於rx為int&類型,代入得int& && var2,折疊后得int& var2 return 0; }
二、完美轉發
(一)std::forward原型
template<typename T> T&& forward(typename remove_reference<T>::type& param) { return static_cast<T&&>(param); //可能會發生引用折疊! }
(二)分析std::forward<T>實現條件轉發的原理(以轉發Widget類對象為例)
1. 當傳遞給func函數的實參類型為左值Widget時,T被推導為Widget&類別。然后forward會實例化為std::forward<Widget&>,並返回Widget&(左值引用,根據定義是個左值!)
2. 而當傳遞給func函數的實參類型為右值Widget時,T被推導為Widget。然后forward被實例化為std::forward<Widget>,並返回Widget&&(注意,匿名的右值引用是個右值!)
3. 可見,std::forward會根據傳遞給func函數實參(注意,不是形參)的左/右值類型進行轉發。當傳給func函數左值實參時,forward返回左值引用,並將該左值轉發給process。而當傳入func的實參為右值時,forward返回右值引用,並將該右值轉發給process函數。
【編程實驗】不完美轉發和完美轉發

#include <iostream> using namespace std; void print(const int& t) //左值版本 { cout <<"void print(const int& t)" << endl; } void print(int&& t) //右值版本 { cout << "void print(int&& t)" << endl; } template<typename T> void testForward(T&& param) { //不完美轉發 print(param); //param為形參,是左值。調用void print(const int& t) print(std::move(param)); //轉為右值。調用void print(int&& t) //完美轉發 print(std::forward<T>(param)); //只有這里才會根據傳入param的實參類型的左右值進轉發 } int main() { cout <<"-------------testForward(1)-------------" <<endl; testForward(1); //傳入右值 cout <<"-------------testForward(x)-------------" << endl; int x = 0; testForward(x); //傳入左值 return 0; } /*輸出結果 -------------testForward(1)------------- void print(const int& t) void print(int&& t) void print(int&& t) //完美轉發,這里轉入的1為右值,調用右值版本的print -------------testForward(x)------------- void print(const int& t) void print(int&& t) void print(const int& t) //完美轉發,這里轉入的x為左值,調用左值版本的print */
三、std::move和std::forward
(一)兩者比較
1. move和forward都是僅僅執行強制類型轉換的函數。std::move無條件地將實參強制轉換成右值。而std::forward則僅在某個特定條件滿足時(傳入func的實參是右值時)才執行強制轉換。
2. std::move並不進行任何移動,std::forward也不進行任何轉發。這兩者在運行期都無所作為。它們不會生成任何可執行代碼,連一個字節都不會生成。
(二)使用時機
1. 針對右值引用的最后一次使用實施std::move,針對萬能引用的最后一次使用實施std::forward。
2. 在按值返回的函數中,如果返回的是一個綁定到右值引用或萬能引用的對象時,可以實施std::move或std::forward。因為如果原始對象是一個右值,它的值就應當被移動到返回值上,而如果是左值,就必須通過復制構造出副本作為返回值。
(三)返回值優化(RVO)
1.兩個前提條件
(1)局部對象類型和函數返回值類型相同;
(2)返回的就是局部對象本身(含局部對象或作為return 語句中的臨時對象等)
2. 注意事項
(1)在RVO的前提條件被滿足時,要么避免復制,要么會自動地用std::move隱式實施於返回值。
(2)按值傳遞的函數形參,把它們作為函數返回值時,情況與返回值優化類似。編譯器這里會選擇第2種處理方案,即返回時將形參轉為右值處理。
(3)如果局部變量有資格進行RVO優化,就不要把std::move或std::forward用在這些局部變量中。因為這可能會讓返回值喪失優化的機會。
【編程實驗】RVO優化和std::move、std::forward
#include <iostream> #include <memory> using namespace std; //1. 針對右值引用實施std::move,針對萬能引用實施std::forward class Data{}; class Widget { std::string name; std::shared_ptr<Data> ptr; public: Widget() { cout <<"Widget()"<<endl; }; //復制構造函數 Widget(const Widget& w):name(w.name), ptr(w.ptr) { cout <<"Widget(const Widget& w)" << endl; } //針對右值引用使用std::move Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr)) { cout << "Widget(Widget&& rhs)" << endl; } //針對萬能引用使用std::forward。 //注意,這里使用萬能引用來替代兩個重載版本:void setName(const string&)和void setName(string&&) //好處就是當使用字符串字面量時,萬能引用版本的效率更高。如w.setName("SantaClaus"),此時字符串會被 //推導為const char(&)[11]類型,然后直接轉給setName函數(可以避免先通過字量面構造臨時string對象)。 //並將該類型直接轉給name的構造函數,節省了一個構造和釋放臨時對象的開銷,效率更高。 template<typename T> void setName(T&& newName) { if (newName != name) { //第1次使用newName name = std::forward<T>(newName); //針對萬能引用的最后一次使用實施forward } } }; //2. 按值返回函數 //2.1 按值返回的是一個綁定到右值引用的對象 class Complex { double x; double y; public: Complex(double x =0, double y=0):x(x),y(y){} Complex& operator+=(const Complex& rhs) { x += rhs.x; y += rhs.y; return *this; } }; Complex operator+(Complex&& lhs, const Complex& rhs) //重載全局operator+ { lhs += rhs; return std::move(lhs); //由於lhs綁定到一個右值引用,這里可以移動到返回值上。 } //2.2 按值返回一個綁定到萬能引用的對象 template<typename T> auto test(T&& t) { return std::forward<T>(t); //由於t是一個萬能引用對象。按值返回時實施std::forward //如果原對象一是個右值,則被移動到返回值上。如果原對象 //是個左值,則會被拷貝到返回值上。 } //3. RVO優化 //3.1 返回局部對象 Widget makeWidget() { Widget w; return w; //返回局部對象,滿足RVO優化兩個條件。為避免復制,會直接在返回值內存上創建w對象。 //但如果改成return std::move(w)時,由於返回值類型不同(Widget右值引用,另一個是Widget) //會剝奪RVO優化的機會,就會先創建w局部對象,再移動給返回值,無形中增加一個移動操作。 //對於這種滿足RVO條件的,當某些情況下無法避免復制的(如多路返回),編譯器仍會默認地對 //將w轉為右值,即return std::move(w),而無須用戶顯式std::move!!! } //3.2 按值形參作為返回值 Widget makeWidget(Widget w) //注意,形參w是按值傳參的。 { //... return w; //這里雖然不滿足RVO條件(w是形參,不是函數內的局部對象),但仍然會被編譯器優化。 //這里會默認地轉換為右值,即return std::move(w) } int main() { cout <<"1. 針對右值引用實施std::move,針對萬能引用實施std::forward" << endl; Widget w; w.setName("SantaClaus"); cout << "2. 按值返回時" << endl; auto t1 = test(w); auto t2 = test(std::move(w)); cout << "3. RVO優化" << endl; Widget w1 = makeWidget(); //按值返回局部對象(RVO) Widget w2 = makeWidget(w1); //按值返回按值形參對象 return 0; } /*輸出結果 1. 針對右值引用實施std::move,針對萬能引用實施std::forward Widget() 2. 按值返回時 Widget(const Widget& w) Widget(Widget&& rhs) 3. RVO優化 Widget() Widget(Widget&& rhs) Widget(const Widget& w) Widget(Widget&& rhs) */
四、完美轉發失敗的情形
(一)完美轉發失敗
1. 完美轉發不僅轉發對象,還轉發其類型、左右值特征以及是否帶有const或volation等修飾詞。而完美轉發的失敗,主要源於模板類型推導失敗或推導的結果是錯誤的類型。
2. 實例說明:假設轉發的目標函數f,而轉發函數為fwd(天然就應該是泛型)。函數如下:
template<typename… Ts> void fwd(Ts&&… params) { f(std::forward<Ts>(params)…); } f(expression); //如果本語句執行了某操作 fwd(expression); //而用同一實參調用fwd則會執行不同操作,則稱完美轉發失敗。
(二)五種完美轉發失敗的情形
1. 使用大括號初始化列表時
(1)失敗原因分析:由於轉發函數是個模板函數,而在模板類型推導中,大括號初始不能自動被推導為std::initializer_list<T>。
(2)解決方案:先用auto聲明一個局部變量,再將該局部變量傳遞給轉發函數。
2. 0和NULL用作空指針時
(1)失敗原因分析:0或NULL以空指針之名傳遞給模板時,類型推導的結果是整型,而不是所希望的指針類型。
(2)解決方案:傳遞nullptr,而非0或NULL。
3. 僅聲明static const 整型成員變量,而無其定義時。
(1)失敗原因分析:C++中常量一般是進入符號表的,只有對其取地址時才會實際分配內存。調用f函數時,其實參是直接從符號表中取值,此時不會發生問題。但當調用fwd時由於其形參是萬能引用,而引用本質上是一個可解引用的指針。因此當傳入fwd時會要求准備某塊內存以供解引用出該變量出來。但因其未定義,也就沒有實際的內存空間, 編譯時可能失敗(取決於編譯器和鏈接器的實現)。
(2)解決方案:在類外定義該成員變量。注意這聲變量在聲明時一般會先給初始值。因此定義時無需也不能再重復指定初始值。
4. 使用重載函數名或模板函數名時
(1)失敗原因分析:由於fwd是個模板函數,其形參沒有任何關於類型的信息。當傳入重載函數名或模板函數(代表許許多多的函數)時,就會導致fwd的形參不知綁定到哪個函數上。
(2)解決方案:在調用fwd調用時手動為形參指定類型信息。
5. 轉發位域時
(1)失敗原因分析:位域是由機器字的若干任意部分組成的(如32位int的第3至5個比特),但這樣的實體是無法直接取地址的。而fwd的形參是個引用,本質上就是指針,所以也沒有辦法創建指向任意比特的指針。
(2)解決方案:制作位域值的副本,並以該副本來調用轉發函數。
【編程實驗】完美轉發失敗的情形及解決方案
#include <iostream> #include <vector> using namespace std; //1. 大括號初始化列表 void f(const std::vector<int>& v) { cout << "void f(const std::vector<int> & v)" << endl; } //2. 0或NULL用作空指針時 void f(int x) { cout << "void f(int x)" << endl; } //3. 僅聲明static const的整型成員變量而無定義 class Widget { public: static const std::size_t MinVals = 28; //僅聲明,無定義(因為靜態變量需在類外定義!) }; //const std::size_t Widget::MinVals; //在類外定義,無須也不能重復指定初始值。 //4. 使用重載函數名或模板函數名 int f(int(*pf)(int)) { cout <<"int f(int(*pf)(int))" << endl; return 0; } int processVal(int value) { return 0; } int processVal(int value, int priority) { return 0; } //5.位域 struct IPv4Header { std::uint32_t version : 4, IHL : 4, DSCP : 6, ECN : 2, totalLength : 16; //... }; template<typename T> T workOnVal(T param) //函數模板,代表許許多多的函數。 { return param; } //用於測試的轉發函數 template<typename ...Ts> void fwd(Ts&& ... param) //轉發函數 { f(std::forward<Ts>(param)...); //目標函數 } int main() { cout <<"-------------------1. 大括號初始化列表---------------------" << endl; //1.1 用同一實參分別調用f和fwd函數 f({ 1, 2, 3 }); //{1, 2, 3}會被隱式轉換為std::vector<int> //fwd({ 1, 2, 3 }); //編譯失敗。由於fwd是個函數模板,而模板推導時{}不能自動被推導為std:;initializer_list<T> //1.2 解決方案 auto il = { 1,2,3 }; fwd(il); cout << "-------------------2. 0或NULL用作空指針-------------------" << endl; //2.1 用同一實參分別調用f和fwd函數 f(NULL); //調用void f(int)函數, fwd(NULL); //NULL被推導為int,仍調用void f(int)函數 //2.2 解決方案:使用nullptr f(nullptr); //匹配int f(int(*pf)(int)) fwd(nullptr); cout << "-------3. 僅聲明static const的整型成員變量而無定義--------" << endl; //3.1 用同一實參分別調用f和fwd函數 f(Widget::MinVals); //調用void f(int)函數。實參從符號表中取得,編譯成功! fwd(Widget::MinVals); //fwd的形參是引用,而引用的本質是指針,但fwd使用到該實參時需要解引用 //這里會因沒有為MinVals分配內存而出現編譯失敗(取決於編譯器和鏈接器) //3.2 解決方案:在類外定義該變量 cout << "-------------4. 使用重載函數名或模板函數名---------------" << endl; //4.1 用同一實參分別調用f和fwd函數 f(processVal); //ok,由於f形參為int(*pf)(int),帶有類型信息,會匹配int processVal(int value) //fwd(processVal); //error,fwd的形參不帶任何類型信息,不知該匹配哪個processVals重載函數。 //fwd(workOnVal); //error,workOnVal是個函數模板,代表許許多多的函數。這里不知綁定到哪個函數 //4.2 解決方案:手動指定類型信息 using ProcessFuncType = int(*)(int); ProcessFuncType processValPtr = processVal; fwd(processValPtr); fwd(static_cast<ProcessFuncType>(workOnVal)); //調用int f(int(*pf)(int)) cout << "----------------------5. 轉發位域時---------------------" << endl; //5.1 用同一實參分別調用f和fwd函數 IPv4Header ip = {}; f(ip.totalLength); //調用void f(int) //fwd(ip.totalLength); //error,fwd形參是引用,由於位域是比特位組成。無法創建比特位的引用! //解決方案:創建位域的副本,並傳給fwd auto length = static_cast<std::uint16_t>(ip.totalLength); fwd(length); return 0; } /*輸出結果 -------------------1. 大括號初始化列表--------------------- void f(const std::vector<int> & v) void f(const std::vector<int> & v) -------------------2. 0或NULL用作空指針------------------- void f(int x) void f(int x) int f(int(*pf)(int)) int f(int(*pf)(int)) -------3. 僅聲明static const的整型成員變量而無定義-------- void f(int x) void f(int x) -------------4. 使用重載函數名或模板函數名--------------- int f(int(*pf)(int)) int f(int(*pf)(int)) int f(int(*pf)(int)) ----------------------5. 轉發位域時--------------------- void f(int x) void f(int x) */