深入理解C++右值引用和移動(dòng)語(yǔ)義:全面解析

C++11
引入了右值引用,它也是C++11
最重要的新特性之一。原因在于它解決了C++
的一大歷史遺留問(wèn)題,即消除了很多場(chǎng)景下的不必要的額外開(kāi)銷(xiāo)。即使你的代碼中并不直接使用右值引用,也可以通過(guò)標(biāo)準(zhǔn)庫(kù),間接地從這一特性中收益。為了更好地理解該特性帶來(lái)的優(yōu)化,以及幫助我們實(shí)現(xiàn)更高效的程序,我們有必要了解一下有關(guān)右值引用的意義。
什么是右值引用
右值
在引入右值的概念前,我們不妨先看看左值。一句話加以概括:左值就是等號(hào)左邊的值;同理,右值也就是等號(hào)右邊的值。舉個(gè)例子:int a = 2;
這里的a
是等號(hào)左邊,可以通過(guò)取址符&
來(lái)獲取地址,所以是一個(gè)左值。而5
在等號(hào)右邊,無(wú)法通過(guò)取址符&
來(lái)獲取地址,所以只一個(gè)右值。
右值引用
左值引用是對(duì)于左值的引用或者叫別名。同樣地,右值引用也就是對(duì)于右值的引用。語(yǔ)法也很簡(jiǎn)單,就是在左值引用的語(yǔ)法之上在多加一個(gè)&
,寫(xiě)成類(lèi)型 &&右值引用名 = 右值;
的形式即可,比如:
int &&a = 5;
a = 6;
string s1 = "hello";
string &&s2 = s1 + s1;
s2 += s1;
上述簡(jiǎn)單例子,展示了右值引用的基本用法。不過(guò)通常情況下,右值引用更多的是被用于處理函數(shù)參數(shù)。比如:
struct Student {
? Student(Student &&s);
};
為什么要使用右值引用
在C++11
之前,很多C++
程序里存在大量的臨時(shí)對(duì)象,又稱(chēng)無(wú)名對(duì)象。主要出現(xiàn)在如下場(chǎng)景:
??函數(shù)的返回值
??用戶自定義類(lèi)型經(jīng)過(guò)一些計(jì)算后產(chǎn)生的臨時(shí)對(duì)象
??值傳遞的形參
先說(shuō)函數(shù)的返回值,最常見(jiàn)的類(lèi)型就是某些返回用戶自定義類(lèi)型的時(shí)候,如果沒(méi)有將其復(fù)制,就會(huì)產(chǎn)生臨時(shí)對(duì)象,比如:
// 返回一個(gè)Student對(duì)象...func1(); ? ? ? ? ? ?
// 調(diào)用了func1創(chuàng)建了一個(gè)Student對(duì)象,但是沒(méi)有使用,于是編譯器創(chuàng)建了一個(gè)臨時(shí)對(duì)象來(lái)進(jìn)行存儲(chǔ)
Student func1();
然后是某些計(jì)算操作后產(chǎn)生的臨時(shí)對(duì)象,比如:
// 編譯器先計(jì)算c1 + c2的結(jié)果,并產(chǎn)生一個(gè)臨時(shí)對(duì)象temp來(lái)存儲(chǔ)結(jié)果,然后計(jì)算temp + c3的結(jié)果,然后將結(jié)果復(fù)制給result
Complex result = c1 + c2 + c3; ?
還有值傳遞的方式的形參,例如:
// 值傳遞...Student stu;func(stu); ?
// 這里相當(dāng)于是做了一次復(fù)制操作 ? Student s(stu);
void func(Student s); ?
而且這些臨時(shí)對(duì)象隨著生命周期的結(jié)束,編譯器還會(huì)調(diào)用一次析構(gòu)函數(shù)。隨著這些操作次數(shù)的增加,或者當(dāng)臨時(shí)變量是個(gè)很大的類(lèi)型時(shí),這無(wú)疑會(huì)極大提高程序的開(kāi)銷(xiāo),從而降低程序的效率。
C++11
之后,隨著右值引用的出現(xiàn),可以有效的解決這些問(wèn)題。通過(guò)move
和移動(dòng)構(gòu)造,移動(dòng)賦值運(yùn)算符函數(shù)來(lái)獲得臨時(shí)對(duì)象的所有權(quán),從而避免拷貝帶來(lái)的額外開(kāi)銷(xiāo),提高程序效率
移動(dòng)構(gòu)造
我們都知道,由于C++11
之前,如果沒(méi)有手動(dòng)聲明,編譯器會(huì)給一個(gè)用于自定義類(lèi)型(包括class
和struct
)自動(dòng)生成的4個(gè)函數(shù),分別是構(gòu)造函數(shù),拷貝構(gòu)造函數(shù),賦值運(yùn)算符重載函數(shù)和析構(gòu)函數(shù)。雖然通過(guò)傳引用的方式,可以避免對(duì)象的復(fù)制。但是還是沒(méi)法避免上述的臨時(shí)對(duì)象的復(fù)制。而移動(dòng)語(yǔ)義成功的解決的這個(gè)問(wèn)題。
在C++11
之后,編譯器自動(dòng)生成的函數(shù)中又新增了2個(gè),它們就是移動(dòng)構(gòu)造和移動(dòng)賦值運(yùn)算符重載函數(shù),通過(guò)它們,我們可以很好地實(shí)現(xiàn)對(duì)用戶自定義類(lèi)型的移動(dòng)操作。而移動(dòng)的本質(zhì)就是獲取臨時(shí)對(duì)象的所有權(quán),而不是通過(guò)復(fù)制的方式來(lái)獲得。直接看代碼:
class Foo {
?public:
? Foo(Foo &&rhs) : ptr_(rhs.ptr_) { rhs.ptr_ = nullptr; }
? Foo &operator=(Foo &&rhs) {
? ? ? if (*this != rhs) {
? ? ? ? ? ptr_ = rhs.ptr_;
? ? ? ? ? rhs.ptr_ = nullptr;
? ? ? }
? ? ? return *this;
? }
?private:
? int *ptr_;
};
Foo類(lèi)重載了移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算重載函數(shù),使得Foo獲得了移動(dòng)的能力,當(dāng)我們?cè)诿鎸?duì)產(chǎn)生臨時(shí)的對(duì)象的時(shí)候,編譯器就會(huì)根據(jù)傳入的參數(shù)是左值還是右值來(lái)選擇調(diào)用拷貝還是移動(dòng)。如果是右值,就調(diào)用移動(dòng)構(gòu)造或移動(dòng)賦值運(yùn)算符函數(shù)。當(dāng)Foo是一個(gè)很大的對(duì)象時(shí)候,就會(huì)極大的降低開(kāi)銷(xiāo),提高程序效率。
move的應(yīng)用場(chǎng)景
通過(guò)上述例子,我們可以看到移動(dòng)并不是說(shuō)完全沒(méi)有開(kāi)銷(xiāo),甚至有的時(shí)候開(kāi)銷(xiāo)并不一定比拷貝低,具體還是要看臨時(shí)對(duì)象的大小和類(lèi)型決定,例如:
vector<vector<int>> func() {
? vector<vector<int>> res;
? for (...) {
? ? ? vector<int> temp;
? ? ? // 沒(méi)必要直接傳就可以了
? ? ? temp.emplace_back(move(5));
? ? ? // 可以,替代了拷貝操作,提高了效率
? ? ? res.emplace_back(move(res));
? }
? return res;
}
STL
的大部分組件都支持移動(dòng)語(yǔ)義,比如vector
,string
等即可以通過(guò)move
轉(zhuǎn)換右值后調(diào)用移動(dòng)構(gòu)造函數(shù)進(jìn)行移動(dòng)操作來(lái)避免深拷貝。還有一些類(lèi)是只允許移動(dòng),不允許拷貝,從而更讓設(shè)計(jì)更符合邏輯,比如unique_ptr
move的原理
move
函數(shù)的源碼并不復(fù)雜:
template <class _Ty>
inline _CONST_FUN typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {
? return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}
我們可以一眼看到,move的實(shí)現(xiàn)其實(shí)就做了一件事,如果是左值,就通過(guò)static_cast
將傳進(jìn)來(lái)的參數(shù)強(qiáng)轉(zhuǎn)為右值并返回;如果是右值,甚至不用轉(zhuǎn)換,直接返回。
右值移動(dòng)的注意事項(xiàng)
??和左值移動(dòng)一樣,都需要直接初始化
??右值引用無(wú)法指向左值,除非使用move將其轉(zhuǎn)成右值,否則編譯報(bào)錯(cuò)
??當(dāng)對(duì)象是基本類(lèi)型的時(shí)候,沒(méi)必要調(diào)用move,因?yàn)榭截惖拈_(kāi)銷(xiāo)可能還不如函數(shù)調(diào)用的開(kāi)銷(xiāo)大,尤其是在循環(huán)內(nèi)的時(shí)候,需要仔細(xì)考慮
??move并不會(huì)一定真的能移動(dòng),它只是將左值強(qiáng)轉(zhuǎn)成右值,只有當(dāng)該用戶自定義類(lèi)型重載了移動(dòng)構(gòu)造和移動(dòng)運(yùn)算符重載函數(shù)時(shí)才會(huì)進(jìn)行移動(dòng)操作
??現(xiàn)代編譯在處理返回值的時(shí)候,通常都會(huì)進(jìn)行返回值優(yōu)化,尤其是標(biāo)準(zhǔn)庫(kù)的組件,使用move來(lái)接收返回值反而會(huì)增加開(kāi)銷(xiāo)
??移動(dòng)之后的對(duì)象就被析構(gòu),所以通常是對(duì)一些臨時(shí)對(duì)象,或者不再使用的對(duì)象進(jìn)行移動(dòng)操作。如果還要繼續(xù)使用該對(duì)象,就要使用拷貝而不是移動(dòng)操作
??右值引用變量本身是個(gè)左值,如果想要右值引用指向右值引用,需要使用move轉(zhuǎn)成右值
??const 左值引用也可以指向右值,但是無(wú)法進(jìn)行修改