一、右值引用
1.右值與右值引用
在C++11中,右值分為兩個(gè)概念:將亡值(xvalue, eXpiring Value)和純右值(prvalue, Pure Rvalue)。
純右值是C++98中右值的概念,表示用于辨識(shí)臨時(shí)變量和一些不跟對(duì)象關(guān)聯(lián)的值,臨時(shí)變量如非引用返回的函數(shù)返回的臨時(shí)變量值、一些運(yùn)算表達(dá)式產(chǎn)生的臨時(shí)變量的值,不跟對(duì)量關(guān)聯(lián)的字面常量如:2、‘c’、true。
將亡值是C++11新增的跟右值引用相關(guān)的表達(dá)式,將亡值可以理解為通過(guò)移動(dòng)構(gòu)造其他變量?jī)?nèi)存空間的方式獲取到的值,在確保其他變量不再被使用、或即將被銷毀時(shí),來(lái)延長(zhǎng)變量值的生命期。而實(shí)際上該右值會(huì)馬上被銷毀,所以稱之為將亡值。
所謂右值引用就是必須綁定到優(yōu)質(zhì)的引用,右值引用的一個(gè)重要特性就是只能綁定到一個(gè)將要銷毀的對(duì)象,因此使用右值引用的代碼可以自由地接管所引用的對(duì)象的資源。
2.為什么有右值引用
假設(shè)定義了函數(shù)void foo(Test &t){...};,通過(guò)傳遞引用來(lái)提高效率,若以foo(Test())的形式調(diào)用函數(shù),會(huì)編譯不過(guò),因?yàn)檫@是將引用綁定到一個(gè)匿名對(duì)象,它可能很快就不在了,所以沒(méi)有意義。為了解決這一問(wèn)題,定義重載函數(shù)void foo(Test t),仍會(huì)編譯不過(guò),因?yàn)檫@樣當(dāng)以foo(t)形式調(diào)用函數(shù)時(shí),會(huì)有二義性。
引入右值引用可以解決上述問(wèn)題,用void foo(Test&& t)和void foo(Test& t)兩個(gè)函數(shù)來(lái)區(qū)分傳入值是否是將亡值,并且可以重載,無(wú)二義性。
3.左值引用與右值引用區(qū)別
左值引用是起別名,如果這個(gè)對(duì)象已經(jīng)析構(gòu),那么這個(gè)別名也應(yīng)該一起失效,也就是說(shuō)左值引用一定要保證它的生命周期小于等于它被引用的對(duì)象。
當(dāng)將亡值出現(xiàn)的時(shí)候,左值引用表示無(wú)能為力,所以右值引用出現(xiàn)了。
右值引用也可以看作起名,只是它起名的對(duì)象是一個(gè)將亡值,然后延續(xù)這個(gè)將亡值的生命,直到這個(gè)的右值的生命也結(jié)束了。
除了入?yún)r(shí)可以用到右值引用外,其他右值引用都顯得多余。
比如Test&& t{Test()};它和Test t{Test()}是一樣的,甚至可以這樣Test&& t{}它們都只一個(gè)普通對(duì)象,只調(diào)用一次構(gòu)造函數(shù)。
二、移動(dòng)語(yǔ)義
1.移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符
拷貝對(duì)象時(shí)在某些情況下,對(duì)象拷貝后就立即被銷毀了,此時(shí)從舊內(nèi)存將元素拷貝到新內(nèi)存是不必要的,更好的方式是移動(dòng)元素。另一方面,對(duì)于IO類、std::unique_ptr這樣的類,他們都包含不能被共享的資源,因此,這些類型的對(duì)象不能拷貝但可以移動(dòng)。
為了讓我們自己的類型支持移動(dòng)操作,需要為其定義移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符。
移動(dòng)構(gòu)造函數(shù)的第一個(gè)參數(shù)是該類類型的一個(gè)引用,且是一個(gè)右值引用。在移動(dòng)構(gòu)造函數(shù)中,獲取了被移動(dòng)對(duì)象的資源(這里是內(nèi)存)的所有權(quán);在接管內(nèi)存之后,將給定對(duì)象中的指針都置為nullptr(使其成為析構(gòu)安全地狀態(tài)),這樣就完成了從給定對(duì)象的移動(dòng)操作。最終,移后源對(duì)象會(huì)被銷毀,意味著在其上運(yùn)行析構(gòu)函數(shù)。
移動(dòng)賦值運(yùn)算符執(zhí)行與析構(gòu)函數(shù)和移動(dòng)構(gòu)造函數(shù)相同的工作。
2.std::move()
std::move(lvalue)的作用是把一個(gè)左值轉(zhuǎn)換為右值。當(dāng)局部變量的左值想移動(dòng)而不是拷貝時(shí),出現(xiàn)std::move將左值轉(zhuǎn)為右值
// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
std::move是一個(gè)模板函數(shù),通過(guò)remove_\reference_t獲得模板參數(shù)的原本類型,然后把值轉(zhuǎn)換為該類型的右值。使用std::move意味著,把一個(gè)左值轉(zhuǎn)換為右值,原先的值不應(yīng)該繼續(xù)再使用。
三、萬(wàn)能引用
1.萬(wàn)能引用
C++ 11中有萬(wàn)能引用(Universal Reference)的概念:使用T&&類型的形參既能綁定右值,又能綁定左值。但是注意了:只有發(fā)生類型推導(dǎo)的時(shí)候,T&&才表示萬(wàn)能引用;否則,表示右值引用。
template<typename T>
void func(T&& param) {
cout << param << endl;
}
int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}
2.引用折疊
一個(gè)模板函數(shù),根據(jù)定義的形參和傳入的實(shí)參的類型,我們可以有下面四中組合:
- 左值-左值
T& &? ? --T&?# 函數(shù)定義的形參類型是左值引用,傳入的實(shí)參是左值引用 - 左值-右值
T& &&?? --T&? # 函數(shù)定義的形參類型是左值引用,傳入的實(shí)參是右值引用 - 右值-左值
T&& &?? --T&? # 函數(shù)定義的形參類型是右值引用,傳入的實(shí)參是左值引用 - 右值-右值
T&& &&? --T&&?# 函數(shù)定義的形參類型是右值引用,傳入的實(shí)參是右值引用
但是C++中不允許對(duì)引用再進(jìn)行引用,對(duì)于上述情況的處理有如下的規(guī)則:
所有的折疊引用最終都代表一個(gè)引用,要么是左值引用,要么是右值引用。規(guī)則是:如果任一引用為左值引用,則結(jié)果為左值引用。否則(即兩個(gè)都是右值引用),結(jié)果為右值引用。
四、完美轉(zhuǎn)發(fā)
所謂轉(zhuǎn)發(fā),就是通過(guò)一個(gè)函數(shù)將參數(shù)繼續(xù)轉(zhuǎn)交給另一個(gè)函數(shù)進(jìn)行處理,原參數(shù)可能是右值,可能是左值,如果還能繼續(xù)保持參數(shù)的原有特征,那么它就是完美的。
一個(gè)例子
template<typename T>
void func(T& param) {
cout << "傳入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "傳入的是右值" << endl;
}
template<typename T>
void warp(T&& param) {
func(param);
}
int main() {
int num = 2019;
warp(num);
warp(2019);
return 0;
}
輸出結(jié)果:
傳入的是左值
傳入的是左值
warp()函數(shù)本身的形參是一個(gè)萬(wàn)能引用,即可以接受左值又可以接受右值;第一個(gè)warp()函數(shù)調(diào)用實(shí)參是左值,所以,warp()函數(shù)中調(diào)用func()中傳入的參數(shù)也應(yīng)該是左值;第二個(gè)warp()函數(shù)調(diào)用實(shí)參是右值,根據(jù)上面所說(shuō)的引用折疊規(guī)則,warp()函數(shù)接收的參數(shù)類型是右值引用,但在warp()函數(shù)內(nèi)部,因?yàn)閰?shù)有了名稱,左值引用類型變?yōu)榱擞抑?,我們也通過(guò)變量名取得變量地址。
這就是我們所謂的“完美轉(zhuǎn)發(fā)”技術(shù),可以保持函數(shù)調(diào)用過(guò)程中,變量類型的不變,在C++11中通過(guò)std::forward()函數(shù)來(lái)實(shí)現(xiàn)。修改warp()函數(shù)如下:
template<typename T>
void warp(T&& param) {
func(std::forward<T>(param));
}
五、總結(jié)
- 右值引用可以解決傳入的函數(shù)參數(shù)是將亡值的問(wèn)題
- 右值引用可能是左值也可能是右值,若這個(gè)右值引用被命名了,它就是左值
- 移動(dòng)語(yǔ)義可以減少無(wú)謂的內(nèi)存拷貝,要想實(shí)現(xiàn)移動(dòng)語(yǔ)義,需要實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符
- 若想對(duì)一個(gè)左值進(jìn)行移動(dòng)構(gòu)造,可用
std::move將一個(gè)左值轉(zhuǎn)換成一個(gè)右值 - 使用
T&&類型的形參既能綁定右值,又能綁定左值。只有發(fā)生類型推導(dǎo)的時(shí)候,才表示萬(wàn)能引用; - 引用折疊規(guī)則:所有的右值引用疊加到右值引用上仍然是一個(gè)右值引用,其他引用折疊都為左值引用。當(dāng)
T&&為模板參數(shù)時(shí),輸入左值,它將變成左值引用,輸入右值則變成具名的右值應(yīng)用。 - 通過(guò)一個(gè)函數(shù)將參數(shù)繼續(xù)轉(zhuǎn)交給另一個(gè)函數(shù)進(jìn)行處理,如果繼續(xù)保持參數(shù)的原有特征,就是完美轉(zhuǎn)發(fā)。
std::forward()和萬(wàn)能引用共同實(shí)現(xiàn)完美轉(zhuǎn)發(fā)。