右值引用、移動(dòng)語(yǔ)義、萬(wàn)能引用與完美轉(zhuǎn)發(fā)

一、右值引用

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ā)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容