1、理解引用摺疊 ios
(一)引用摺疊express
1. 在C++中,「引用的引用」是非法的。像auto& &rx = x;(注意兩個&之間有空格)這種直接定義引用的引用是不合法的,可是編譯器在經過類型別名或模板參數推導等語境中,會間接定義出「引用的引用」,這時引用會造成「摺疊」。編程
2. 引用摺疊會發生在模板實例化、auto類型推導、建立和運用typedef和別名聲明、以及decltype語境中。ide
(二)引用摺疊規則函數
1. 兩條規則測試
(1)全部右值引用摺疊到右值引用上仍然是一個右值引用。如X&& &&摺疊爲X&&。優化
(2)全部的其餘引用類型之間的摺疊都將變成左值引用。如X& &, X& &&, X&& &摺疊爲X&。可見左值引用會傳染,沾上一個左值引用就變左值引用了。根本緣由:在一處聲明爲左值,就說明該對象爲持久對象,編譯器就必須保證此對象可靠(左值)。ui
2. 利用引用摺疊進行萬能引用初始化類型推導this
(1)當萬能引用(T&& param)綁定到左值時,因爲萬能引用也是一個引用,而左值只能綁定到左值引用。所以,T會被推導爲T&類型。從而param的類型爲T& &&,引用摺疊後的類型爲T&。spa
(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; }
2、完美轉發
(一)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 */
3、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) */
4、完美轉發失敗的情形
(一)完美轉發失敗
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) */