C++右值引用與移動(dòng)構(gòu)造函數(shù)基礎(chǔ)與應(yīng)用詳解
1.右值引用
右值引用是 C++11 引入的與 Lambda 表達(dá)式齊名的重要特性之一。它的引入解決了 C++ 中大量的歷史遺留問(wèn)題, 消除了諸如 std::vector、std::string 之類(lèi)的額外開(kāi)銷(xiāo), 也才使得函數(shù)對(duì)象容器 std::function 成為了可能。
1.1左值右值的純右值將亡值右值
要弄明白右值引用到底是怎么一回事,必須要對(duì)左值和右值做一個(gè)明確的理解。
左值 (lvalue, left value),顧名思義就是賦值符號(hào)左邊的值。準(zhǔn)確來(lái)說(shuō), 左值是表達(dá)式(不一定是賦值表達(dá)式)后依然存在的持久對(duì)象。
右值 (rvalue, right value),右邊的值,是指表達(dá)式結(jié)束后就不再存在的臨時(shí)對(duì)象。
而 C++11 中為了引入強(qiáng)大的右值引用,將右值的概念進(jìn)行了進(jìn)一步的劃分,分為:純右值、將亡值。
純右值 (prvalue, pure rvalue),純粹的右值,要么是純粹的字面量,例如 10, true; 要么是求值結(jié)果相當(dāng)于字面量或匿名臨時(shí)對(duì)象,例如 1+2。非引用返回的臨時(shí)變量、運(yùn)算表達(dá)式產(chǎn)生的臨時(shí)變量、 原始字面量、Lambda 表達(dá)式都屬于純右值。
需要注意的是,字面量除了字符串字面量以外,均為純右值。而字符串字面量是一個(gè)左值,類(lèi)型為 const char 數(shù)組。例如:
#include <type_traits> int main() { // 正確,"01234" 類(lèi)型為 const char [6],因此是左值 const char (&left)[6] = "01234"; // 斷言正確,確實(shí)是 const char [6] 類(lèi)型,注意 decltype(expr) 在 expr 是左值 // 且非無(wú)括號(hào)包裹的 id 表達(dá)式與類(lèi)成員表達(dá)式時(shí),會(huì)返回左值引用 static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, ""); // 錯(cuò)誤,"01234" 是左值,不可被右值引用 // const char (&&right)[6] = "01234"; }
但是注意,數(shù)組可以被隱式轉(zhuǎn)換成相對(duì)應(yīng)的指針類(lèi)型,而轉(zhuǎn)換表達(dá)式的結(jié)果(如果不是左值引用)則一定是個(gè)右值(右值引用為將亡值,否則為純右值)。例如:
const char* p = "01234"; // 正確,"01234" 被隱式轉(zhuǎn)換為 const char* const char*&& pr = "01234"; // 正確,"01234" 被隱式轉(zhuǎn)換為 const char*,該轉(zhuǎn)換的結(jié)果是純右值 // const char*& pl = "01234"; // 錯(cuò)誤,此處不存在 const char* 類(lèi)型的左值 將亡值 (xvalue, expiring value),是 C++11 為了引入右值引用而提出的概念(因此在傳統(tǒng) C++ 中, 純右值和右值是同一個(gè)概念),也就是即將被銷(xiāo)毀、卻能夠被移動(dòng)的值。 將亡值可能稍有些難以理解,我們來(lái)看這樣的代碼: std::vector<int> foo() { std::vector<int> temp = {1, 2, 3, 4}; return temp; } std::vector<int> v = foo();
在這樣的代碼中,就傳統(tǒng)的理解而言,函數(shù) foo 的返回值 temp 在內(nèi)部創(chuàng)建然后被賦值給 v, 然而 v 獲得這個(gè)對(duì)象時(shí),會(huì)將整個(gè) temp 拷貝一份,然后把 temp 銷(xiāo)毀,如果這個(gè) temp 非常大, 這將造成大量額外的開(kāi)銷(xiāo)(這也就是傳統(tǒng) C++ 一直被詬病的問(wèn)題)。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是純右值)。但是,v 可以被別的變量捕獲到, 而 foo() 產(chǎn)生的那個(gè)返回值作為一個(gè)臨時(shí)值,一旦被 v 復(fù)制后,將立即被銷(xiāo)毀,無(wú)法獲取、也不能修改。 而將亡值就定義了這樣一種行為:臨時(shí)的值能夠被識(shí)別、同時(shí)又能夠被移動(dòng)。
在 C++11 之后,編譯器為我們做了一些工作,此處的左值 temp 會(huì)被進(jìn)行此隱式右值轉(zhuǎn)換, 等價(jià)于 static_cast<std::vector<int> &&>(temp),進(jìn)而此處的 v 會(huì)將 foo 局部返回的值進(jìn)行移動(dòng)。 也就是后面我們將會(huì)提到的移動(dòng)語(yǔ)義。
1.2右值引用和左值引用
要拿到一個(gè)將亡值,就需要用到右值引用:T &&,其中 T 是類(lèi)型。 右值引用的聲明讓這個(gè)臨時(shí)值的生命周期得以延長(zhǎng)、只要變量還活著,那么將亡值將繼續(xù)存活。
C++11 提供了 std::move 這個(gè)方法將左值參數(shù)無(wú)條件的轉(zhuǎn)換為右值, 有了它我們就能夠方便的獲得一個(gè)右值臨時(shí)對(duì)象,例如:
#include <iostream> #include <string> void reference(std::string& str) { std::cout << "左值" << std::endl; } void reference(std::string&& str) { std::cout << "右值" << std::endl; } int main() { std::string lv1 = "string,"; // lv1 是一個(gè)左值 // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值 std::string&& rv1 = std::move(lv1); // 合法, std::move可以將左值轉(zhuǎn)移為右值 std::cout << rv1 << std::endl; // string, const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能夠延長(zhǎng)臨時(shí)變量的生命周期 // lv2 += "Test"; // 非法, 常量引用無(wú)法被修改 std::cout << lv2 << std::endl; // string,string, std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延長(zhǎng)臨時(shí)對(duì)象生命周期 rv2 += "Test"; // 合法, 非常量引用能夠修改臨時(shí)變量 std::cout << rv2 << std::endl; // string,string,string,Test reference(rv2); // 輸出左值 return 0; }
rv2 雖然引用了一個(gè)右值,但由于它是一個(gè)引用,所以 rv2 依然是一個(gè)左值。
注意,這里有一個(gè)很有趣的歷史遺留問(wèn)題,我們先看下面的代碼:
#include <iostream> int main() { // int &a = std::move(1); // 不合法,非常量左引用無(wú)法引用右值 const int &b = std::move(1); // 合法, 常量左引用允許引用右值 std::cout << a << b << std::endl; } 第一個(gè)問(wèn)題,為什么不允許非常量引用綁定到非左值?這是因?yàn)檫@種做法存在邏輯錯(cuò)誤: void increase(int & v) { v++; } void foo() { double s = 1; increase(s); }
由于 int& 不能引用 double 類(lèi)型的參數(shù),因此必須產(chǎn)生一個(gè)臨時(shí)值來(lái)保存 s 的值, 從而當(dāng) increase() 修改這個(gè)臨時(shí)值時(shí),調(diào)用完成后 s 本身并沒(méi)有被修改。
第二個(gè)問(wèn)題,為什么常量引用允許綁定到非左值?原因很簡(jiǎn)單,因?yàn)?Fortran 需要。
2.移動(dòng)構(gòu)造函數(shù)
傳統(tǒng) C++ 通過(guò)拷貝構(gòu)造函數(shù)和賦值操作符為類(lèi)對(duì)象設(shè)計(jì)了拷貝/復(fù)制的概念,但為了實(shí)現(xiàn)對(duì)資源的移動(dòng)操作, 調(diào)用者必須使用先復(fù)制、再析構(gòu)的方式,否則就需要自己實(shí)現(xiàn)移動(dòng)對(duì)象的接口。 試想,搬家的時(shí)候是把家里的東西直接搬到新家去,而不是將所有東西復(fù)制一份(重買(mǎi))再放到新家、 再把原來(lái)的東西全部扔掉(銷(xiāo)毀)
傳統(tǒng)的 C++ 沒(méi)有區(qū)分『移動(dòng)』和『拷貝』的概念,造成了大量的數(shù)據(jù)拷貝,浪費(fèi)時(shí)間和空間。 右值引用的出現(xiàn)恰好就解決了這兩個(gè)概念的混淆問(wèn)題,例如:
#include <iostream> class A { public: int *pointer; A():pointer(new int(1)) { std::cout << "構(gòu)造" << pointer << std::endl; } A(A& a):pointer(new int(*a.pointer)) { std::cout << "拷貝" << pointer << std::endl; } // 無(wú)意義的對(duì)象拷貝 A(A&& a):pointer(a.pointer) { a.pointer = nullptr; std::cout << "移動(dòng)" << pointer << std::endl; } ~A(){ std::cout << "析構(gòu)" << pointer << std::endl; delete pointer; } }; // 防止編譯器優(yōu)化 A return_rvalue(bool test) { A a,b; if(test) return a; // 等價(jià)于 static_cast<A&&>(a); else return b; // 等價(jià)于 static_cast<A&&>(b); } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; }
在上面的代碼中:
首先會(huì)在 return_rvalue 內(nèi)部構(gòu)造兩個(gè) A 對(duì)象,于是獲得兩個(gè)構(gòu)造函數(shù)的輸出;
函數(shù)返回后,產(chǎn)生一個(gè)將亡值,被 A 的移動(dòng)構(gòu)造(A(A&&))引用,從而延長(zhǎng)生命周期,并將這個(gè)右值中的指針拿到,保存到了 obj 中,而將亡值的指針被設(shè)置為 nullptr,防止了這塊內(nèi)存區(qū)域被銷(xiāo)毀。
從而避免了無(wú)意義的拷貝構(gòu)造,加強(qiáng)了性能。再來(lái)看看涉及標(biāo)準(zhǔn)庫(kù)的例子:
#include <iostream> // std::cout #include <utility> // std::move #include <vector> // std::vector #include <string> // std::string int main() { std::string str = "Hello world."; std::vector<std::string> v; // 將使用 push_back(const T&), 即產(chǎn)生拷貝行為 v.push_back(str); // 將輸出 "str: Hello world." std::cout << "str: " << str << std::endl; // 將使用 push_back(const T&&), 不會(huì)出現(xiàn)拷貝行為 // 而整個(gè)字符串會(huì)被移動(dòng)到 vector 中,所以有時(shí)候 std::move 會(huì)用來(lái)減少拷貝出現(xiàn)的開(kāi)銷(xiāo) // 這步操作后, str 中的值會(huì)變?yōu)榭? v.push_back(std::move(str)); // 將輸出 "str: " std::cout << "str: " << str << std::endl; return 0; }
2.1完美的移動(dòng)轉(zhuǎn)發(fā)
前面我們提到了,一個(gè)聲明的右值引用其實(shí)是一個(gè)左值。這就為我們進(jìn)行參數(shù)轉(zhuǎn)發(fā)(傳遞)造成了問(wèn)題:
void reference(int& v) { std::cout << "左值" << std::endl; } void reference(int&& v) { std::cout << "右值" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通傳參:"; reference(v); // 始終調(diào)用 reference(int&) } int main() { std::cout << "傳遞右值:" << std::endl; pass(1); // 1是右值, 但輸出是左值 std::cout << "傳遞左值:" << std::endl; int l = 1; pass(l); // l 是左值, 輸出左值 return 0; }
對(duì)于 pass(1) 來(lái)說(shuō),雖然傳遞的是右值,但由于 v 是一個(gè)引用,所以同時(shí)也是左值。 因此 reference(v) 會(huì)調(diào)用 reference(int&),輸出『左值』。 而對(duì)于pass(l)而言,l是一個(gè)左值,為什么會(huì)成功傳遞給 pass(T&&) 呢?
這是基于引用坍縮規(guī)則的:在傳統(tǒng) C++ 中,我們不能夠?qū)σ粋€(gè)引用類(lèi)型繼續(xù)進(jìn)行引用, 但 C++ 由于右值引用的出現(xiàn)而放寬了這一做法,從而產(chǎn)生了引用坍縮規(guī)則,允許我們對(duì)引用進(jìn)行引用, 既能左引用,又能右引用。但是卻遵循如下規(guī)則:
函數(shù)形參類(lèi)型 | 實(shí)參參數(shù)類(lèi)型 | 推導(dǎo)后函數(shù)形參類(lèi)型 |
T& | 左引用 | T& |
T& | 右引用 | T& |
T&& | 左引用 | T& |
T&& | 右引用 | T&& |
因此,模板函數(shù)中使用 T&& 不一定能進(jìn)行右值引用,當(dāng)傳入左值時(shí),此函數(shù)的引用將被推導(dǎo)為左值。 更準(zhǔn)確的講,無(wú)論模板參數(shù)是什么類(lèi)型的引用,當(dāng)且僅當(dāng)實(shí)參類(lèi)型為右引用時(shí),模板參數(shù)才能被推導(dǎo)為右引用類(lèi)型。 這才使得 v 作為左值的成功傳遞。
完美轉(zhuǎn)發(fā)就是基于上述規(guī)律產(chǎn)生的。所謂完美轉(zhuǎn)發(fā),就是為了讓我們?cè)趥鬟f參數(shù)的時(shí)候, 保持原來(lái)的參數(shù)類(lèi)型(左引用保持左引用,右引用保持右引用)。 為了解決這個(gè)問(wèn)題,我們應(yīng)該使用 std::forward 來(lái)進(jìn)行參數(shù)的轉(zhuǎn)發(fā)(傳遞):
#include <iostream> #include <utility> void reference(int& v) { std::cout << "左值引用" << std::endl; } void reference(int&& v) { std::cout << "右值引用" << std::endl; } template <typename T> void pass(T&& v) { std::cout << " 普通傳參: "; reference(v); std::cout << " std::move 傳參: "; reference(std::move(v)); std::cout << " std::forward 傳參: "; reference(std::forward<T>(v)); std::cout << "static_cast<T&&> 傳參: "; reference(static_cast<T&&>(v)); } int main() { std::cout << "傳遞右值:" << std::endl; pass(1); std::cout << "傳遞左值:" << std::endl; int v = 1; pass(v); return 0; }
輸出結(jié)果為:
傳遞右值:
普通傳參: 左值引用
std::move 傳參: 右值引用
std::forward 傳參: 右值引用
static_cast<T&&> 傳參: 右值引用
傳遞左值:
普通傳參: 左值引用
std::move 傳參: 右值引用
std::forward 傳參: 左值引用
static_cast<T&&> 傳參: 左值引用
無(wú)論傳遞參數(shù)為左值還是右值,普通傳參都會(huì)將參數(shù)作為左值進(jìn)行轉(zhuǎn)發(fā), 所以 std::move 總會(huì)接受到一個(gè)左值,從而轉(zhuǎn)發(fā)調(diào)用了reference(int&&) 輸出右值引用。
唯獨(dú) std::forward 即沒(méi)有造成任何多余的拷貝,同時(shí)完美轉(zhuǎn)發(fā)(傳遞)了函數(shù)的實(shí)參給了內(nèi)部調(diào)用的其他函數(shù)。
std::forward 和 std::move 一樣,沒(méi)有做任何事情,std::move 單純的將左值轉(zhuǎn)化為右值, std::forward 也只是單純的將參數(shù)做了一個(gè)類(lèi)型的轉(zhuǎn)換,從現(xiàn)象上來(lái)看, std::forward<T>(v) 和 static_cast<T&&>(v) 是完全一樣的。
讀者可能會(huì)好奇,為何一條語(yǔ)句能夠針對(duì)兩種類(lèi)型的返回對(duì)應(yīng)的值, 我們?cè)俸?jiǎn)單看一看 std::forward 的具體實(shí)現(xiàn)機(jī)制,std::forward 包含兩個(gè)重載:
template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); } template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept { static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument" " substituting _Tp is an lvalue reference type"); return static_cast<_Tp&&>(__t); }
在這份實(shí)現(xiàn)中,std::remove_reference 的功能是消除類(lèi)型中的引用, std::is_lvalue_reference 則用于檢查類(lèi)型推導(dǎo)是否正確,在 std::forward 的第二個(gè)實(shí)現(xiàn)中 檢查了接收到的值確實(shí)是一個(gè)左值,進(jìn)而體現(xiàn)了坍縮規(guī)則。
當(dāng) std::forward 接受左值時(shí),_Tp 被推導(dǎo)為左值,所以返回值為左值;而當(dāng)其接受右值時(shí), _Tp 被推導(dǎo)為 右值引用,則基于坍縮規(guī)則,返回值便成為了 && + && 的右值。 可見(jiàn) std::forward 的原理在于巧妙的利用了模板類(lèi)型推導(dǎo)中產(chǎn)生的差異。
這時(shí)我們能回答這樣一個(gè)問(wèn)題:為什么在使用循環(huán)語(yǔ)句的過(guò)程中,auto&& 是最安全的方式? 因?yàn)楫?dāng) auto 被推導(dǎo)為不同的左右引用時(shí),與 && 的坍縮組合是完美轉(zhuǎn)發(fā)。
到此這篇關(guān)于C++右值引用與移動(dòng)構(gòu)造函數(shù)基礎(chǔ)與應(yīng)用詳解的文章就介紹到這了,更多相關(guān)C++右值引用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言 自增自減運(yùn)算的區(qū)別詳解及實(shí)例
這篇文章主要介紹了C語(yǔ)言中的++a和a++的區(qū)別詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-05-05C語(yǔ)言sizeof和strlen的指針和數(shù)組面試題詳解
strlen是函數(shù),字符串長(zhǎng)度,不包括停止符。而sizeof則是內(nèi)存塊的大小,包括停止符。數(shù)組是一種數(shù)據(jù)類(lèi)型,數(shù)據(jù)類(lèi)型的本質(zhì)就是固定大小,內(nèi)存塊的別名。可以用sizeof()一般都是數(shù)據(jù)類(lèi)型2022-04-04C++ Eigen庫(kù)計(jì)算矩陣特征值及特征向量
這篇文章主要為大家詳細(xì)介紹了C++ Eigen庫(kù)計(jì)算矩陣特征值及特征向量,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06C語(yǔ)言實(shí)現(xiàn)各種排序算法實(shí)例代碼(選擇,冒泡,插入,歸并,希爾,快排,堆排序,計(jì)數(shù))
排序算法是算法之中相對(duì)基礎(chǔ)的,也是各門(mén)語(yǔ)言的必學(xué)的算法,這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)各種排序算法(選擇,冒泡,插入,歸并,希爾,快排,堆排序,計(jì)數(shù))的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-10-10C語(yǔ)言代碼實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言代碼實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06C++類(lèi)成員函數(shù)中的名字查找問(wèn)題
這篇文章主要介紹了C++類(lèi)成員函數(shù)中的名字查找問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11C語(yǔ)言中settimeofday函數(shù)和gettimeofday函數(shù)的使用
這篇文章主要介紹了C語(yǔ)言中的settimeofday函數(shù)和gettimeofday函數(shù)的使用,注意settimeofday()函數(shù)只返回0和-1,需要的朋友可以參考下2015-08-08C++設(shè)計(jì)模式編程中proxy代理模式的使用實(shí)例
這篇文章主要介紹了C++設(shè)計(jì)模式編程中proxy代理模式的使用實(shí)例解析,代理模式可以被歸類(lèi)為結(jié)構(gòu)型的設(shè)計(jì)模式,代理模式主張為對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問(wèn),需要的朋友可以參考下2016-03-03