一直以來(lái),C++中基于值語(yǔ)義的拷貝和賦值嚴(yán)重影響了程序性能。尤其是對(duì)于資源密集型對(duì)象,如果進(jìn)行大量的拷貝,勢(shì)必會(huì)對(duì)程序性能造成很大的影響。
為了盡可能的減小因?yàn)閷?duì)象拷貝對(duì)程序的影響,開(kāi)發(fā)人員使出了萬(wàn)般招式:盡可能的使用指針、引用。而編譯器也沒(méi)閑著,通過(guò)使用RVO、NRVO以及復(fù)制省略技術(shù),來(lái)減小拷貝次數(shù)來(lái)提升代碼的運(yùn)行效率。
但是,對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),使用指針和引用不能概括所有的場(chǎng)景,也就是說(shuō)仍然存在拷貝賦值等行為;對(duì)于編譯器來(lái)說(shuō),而對(duì)于RVO、NRVO等編譯器行為的優(yōu)化需要滿足特定的條件(具體可以參考文章編譯器之返回值優(yōu)化)。
為了解決上述問(wèn)題,自C++11起,引入了移動(dòng)語(yǔ)義,更進(jìn)一步對(duì)程序性能進(jìn)行優(yōu)化 。
C++11新標(biāo)準(zhǔn)重新定義了lvalue和rvalue,并允許函數(shù)依照這兩種不同的類型進(jìn)行重載。通過(guò)對(duì)于右值(rvalue)的重新定義,語(yǔ)言實(shí)現(xiàn)了移動(dòng)語(yǔ)義(move semantics)和完美轉(zhuǎn)發(fā)(perfect forwarding),通過(guò)這種方法,C++實(shí)現(xiàn)了在保留原有的語(yǔ)法并不改動(dòng)已存在的代碼的基礎(chǔ)上提升代碼性能的目的。
本文的主要內(nèi)容如下圖所示:

值語(yǔ)義
值語(yǔ)義(value semantics)指目標(biāo)對(duì)象由源對(duì)象拷貝生成,且生成后與源對(duì)象完全無(wú)關(guān),彼此獨(dú)立存在,改變互不影響,就像int類型互相拷貝一樣。C++的內(nèi)置類型(bool/int/double/char)都是值語(yǔ)義,標(biāo)準(zhǔn)庫(kù)里的complex<> 、pair<>、vector<>、map<>、string等等類型也都是值語(yǔ)意,拷貝之后就與原對(duì)象脫離關(guān)系。
C++中基于值語(yǔ)義的拷貝構(gòu)造和賦值拷貝,會(huì)招致對(duì)資源密集型對(duì)象不必要拷貝,大量的拷貝很可能成為程序的性能瓶頸。
首先,我們看一段例子:
BigObjfun(BigObjobj){
BigObjo;
//dosth
returno;
}
intmain(){
fun(BigObj());
return0;
}
在上述代碼中,我們定義了一個(gè)函數(shù)fun()其參數(shù)是一個(gè)BigObj對(duì)象,當(dāng)調(diào)用fun()函數(shù)時(shí)候,會(huì)通過(guò)調(diào)用BigObj的拷貝構(gòu)造函數(shù),將obj變量傳遞給fun()的參數(shù)。
編譯器知道何時(shí)調(diào)用拷貝構(gòu)造函數(shù)或者賦值運(yùn)算符進(jìn)行值傳遞。如果涉及到底層資源,比如內(nèi)存、socket等,開(kāi)發(fā)人在定義類的時(shí)候,需要實(shí)現(xiàn)自己的拷貝構(gòu)造和賦值運(yùn)算符以實(shí)現(xiàn)深拷貝
。然而拷貝的代價(jià)很大,當(dāng)我們使用STL容器的時(shí)候,都會(huì)涉及到大量的拷貝操作,而這些會(huì)浪費(fèi)CPU和內(nèi)存等資源。
正如上述代碼中所示的那樣,當(dāng)我們將一個(gè)臨時(shí)變量(BigObj(),也稱為右值)傳遞給一個(gè)函數(shù)的時(shí)候,就會(huì)導(dǎo)致拷貝操作,那么我們?cè)撊绾伪苊獯朔N拷貝行為呢?這就是我們本文的主題:移動(dòng)語(yǔ)義
。
左值、右值
關(guān)于左值、右值,我們?cè)谥暗奈恼轮幸呀?jīng)有過(guò)詳細(xì)的分享,有興趣的同學(xué)可以移步【Modern C++】深入理解左值、右值,在本節(jié),我們簡(jiǎn)單介紹下左值和右值的概念,以方便理解下面的內(nèi)容。
左值(lvalue,left value),顧名思義就是賦值符號(hào)左邊的值。準(zhǔn)確來(lái)說(shuō),左值是表達(dá)式結(jié)束(不一定是賦值表達(dá)式)后依然存在的對(duì)象。
可以將左值看作是一個(gè)關(guān)聯(lián)了名稱的內(nèi)存位置,允許程序的其他部分來(lái)訪問(wèn)它。在這里,我們將 "名稱" 解釋為任何可用于訪問(wèn)內(nèi)存位置的表達(dá)式。所以,如果 arr 是一個(gè)數(shù)組,那么 arr[1] 和 *(arr+1) 都將被視為相同內(nèi)存位置的“名稱”。
左值具有以下特征:
- 可通過(guò)取地址運(yùn)算符獲取其地址
- 可修改的左值可用作內(nèi)建賦值和內(nèi)建符合賦值運(yùn)算符的左操作數(shù)
- 可以用來(lái)初始化左值引用(后面有講)
C++11將右值分為純右值和將亡值兩種。純右值就是C++98標(biāo)準(zhǔn)中右值的概念,如非引用返回的函數(shù)返回的臨時(shí)變量值;一些運(yùn)算表達(dá)式,如1+2產(chǎn)生的臨時(shí)變量;不跟對(duì)象關(guān)聯(lián)的字面量值,如2,'c',true,"hello";這些值都不能夠被取地址。
而將亡值則是C++11新增的和右值引用相關(guān)的表達(dá)式,這樣的表達(dá)式通常是將要移動(dòng)的對(duì)象、T&&函數(shù)返回值、std::move()函數(shù)的返回值等。
左值引用、右值引用
在明確了左值和右值的概念之后,我們將在本節(jié)簡(jiǎn)單介紹下左值引用和右值引用。
按照概念,對(duì)左值的引用稱為左值引用,而對(duì)右值的引用稱為右值引用。既然有了左值引用和右值引用,那么在C++11之前,我們通常所說(shuō)的引用又是什么呢?其實(shí)就是左值引用,比如:
inta=1;
int&b=a;
在C++11之前,我們通過(guò)會(huì)說(shuō)b是對(duì)a的一個(gè)引用(當(dāng)然,在C++11及以后也可以這么說(shuō),大家潛移默化的認(rèn)識(shí)就是引用==左值引用),但是在C++11中,更為精確的說(shuō)法是b是一個(gè)左值引用。
在C++11中,為了區(qū)分左值引用,右值引用用&&來(lái)表示,如下:
int&&a=1;//a是一個(gè)左值引用
intb=1;
int&&c=b;//錯(cuò)誤,右值引用不能綁定左值
跟左值引用一樣,右值引用不會(huì)發(fā)生拷貝,并且右值引用等號(hào)右邊必須是右值,如果是左值則會(huì)編譯出錯(cuò),當(dāng)然這里也可以進(jìn)行強(qiáng)制轉(zhuǎn)換,這將在后面提到。
在這里,有一個(gè)大家都經(jīng)常容易犯的一個(gè)錯(cuò)誤,就是綁定右值的右值引用,其變量本身是個(gè)左值。為了便于理解,代碼如下:
intfun(int&a){
std::cout<"infun(int&)"<std::endl;
}
intfun(int&&a){
std::cout<"infun(int&)"<std::endl;
}
intmain(){
inta=1;
int&&b=1;
fun(b);
return0;
}
代碼輸出如下:
infun(int&)
左值引用和右值引用的規(guī)則如下:
- 左值引用,使用T&,只能綁定左值
- 右值引用,使用T&&,只能綁定右值
- 常量左值,使用const T&,既可以綁定左值,又可以綁定右值,但是不能對(duì)其進(jìn)行修改
- 具名右值引用,編譯器會(huì)認(rèn)為是個(gè)左值
- 編譯器的優(yōu)化需要滿足特定條件,不能過(guò)度依賴
好了,截止到目前,相信你對(duì)左值引用和右值引用的概念有了初步的認(rèn)識(shí),那么,現(xiàn)在我們介紹下為什么要有右值引用呢?我們看下述代碼:
BigObjfun(){
returnBigObj();
}
BigObjobj=fun();//C++11以前
BigObj&&obj=fun();//C++11
上述代碼中,在C++11之前,我們只能通過(guò)編譯器優(yōu)化(N)RVO的方式來(lái)提升性能,如果不滿足編譯器的優(yōu)化條件,則只能通過(guò)拷貝等方式進(jìn)行操作。自C++11引入右值引用后,對(duì)于不滿足(N)RVO條件,也可以通過(guò)避免拷貝延長(zhǎng)臨時(shí)變量的生命周期,進(jìn)而達(dá)到優(yōu)化的目的。
但是僅僅使用右值引用還不足以完全達(dá)到優(yōu)化目的,畢竟右值引用只能綁定右值。那么,對(duì)于左值,我們又該如何優(yōu)化呢?是否可以通過(guò)左值轉(zhuǎn)成右值,然后進(jìn)行優(yōu)化呢?等等
為了解決上述問(wèn)題,標(biāo)準(zhǔn)引入了移動(dòng)語(yǔ)義。通移動(dòng)語(yǔ)義,可以在必要的時(shí)候避免拷貝;標(biāo)準(zhǔn)提供了move()函數(shù),可以將左值轉(zhuǎn)換成右值。接下來(lái),就開(kāi)始我們本文的重點(diǎn)-移動(dòng)語(yǔ)義。
移動(dòng)語(yǔ)義
移動(dòng)語(yǔ)義是Howard Hinnant在2002年向C++標(biāo)準(zhǔn)委員會(huì)提議的,引用其在移動(dòng)語(yǔ)義提案上的一句話:
移動(dòng)語(yǔ)義不是試圖取代復(fù)制語(yǔ)義,也不是以任何方式破壞它。相反,該提議旨在增強(qiáng)復(fù)制語(yǔ)義
對(duì)于剛剛接觸移動(dòng)語(yǔ)義的開(kāi)發(fā)人員來(lái)說(shuō),很難理解為什么有了值語(yǔ)義還需要有移動(dòng)語(yǔ)義。
我們可以想象一下,有一輛汽車(chē),在內(nèi)置發(fā)動(dòng)機(jī)的情況下運(yùn)行平穩(wěn),有一天,在這輛車(chē)上安裝了一個(gè)額外的V8發(fā)動(dòng)機(jī)。當(dāng)有足夠燃料的時(shí)候,V8發(fā)動(dòng)機(jī)就能進(jìn)行加速。所以,汽車(chē)是值語(yǔ)義,而V8引擎則是移動(dòng)語(yǔ)義。在車(chē)上安裝引擎不需要一輛新車(chē),它仍然是同一輛車(chē),就像移動(dòng)語(yǔ)義不會(huì)放棄值語(yǔ)義一樣。
所以,如果可以,使用移動(dòng)語(yǔ)義,否則使用值語(yǔ)義,換句話說(shuō)就是,如果燃料充足,則使用V8引擎,否則使用原始默認(rèn)引擎。
好了,截止到現(xiàn)在,我們對(duì)移動(dòng)語(yǔ)義有一個(gè)感官上的認(rèn)識(shí),它屬于一種優(yōu)化,或者說(shuō)屬于錦上添花。再次引用Howard Hinnant在移動(dòng)語(yǔ)義提案上的一句話:
移動(dòng)語(yǔ)義主要是性能優(yōu)化:將昂貴的對(duì)象從內(nèi)存中的一個(gè)地址移動(dòng)到另外一個(gè)地址的能力,同時(shí)竊取源資源以便以最小的代價(jià)構(gòu)建目標(biāo)
在C++11之前,當(dāng)進(jìn)行值傳遞時(shí),編譯器會(huì)隱式調(diào)用拷貝構(gòu)造函數(shù);自C++11起,通過(guò)右值引用來(lái)避免由于拷貝調(diào)用而導(dǎo)致的性能損失。
右值引用的主要用途是創(chuàng)建移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符
。移動(dòng)構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù)一樣,將對(duì)象的實(shí)例作為其參數(shù),并從原始對(duì)象創(chuàng)建一個(gè)新的實(shí)例。
但是,移動(dòng)構(gòu)造函數(shù)可以避免內(nèi)存重新分配
,這是因?yàn)橐苿?dòng)構(gòu)造函數(shù)的參數(shù)是一個(gè)右值引用,也可以說(shuō)是一個(gè)臨時(shí)對(duì)象
,而臨時(shí)對(duì)象在調(diào)用之后就被銷毀不再被使用,因此,在移動(dòng)構(gòu)造函數(shù)中對(duì)參數(shù)進(jìn)行移動(dòng)而不是拷貝。換句話說(shuō),右值引用和移動(dòng)語(yǔ)義允許我們?cè)谑褂门R時(shí)對(duì)象時(shí)避免不必要的拷貝。
移動(dòng)語(yǔ)義通過(guò)移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值操作符實(shí)現(xiàn),其與拷貝構(gòu)造函數(shù)類似,區(qū)別如下:
- 參數(shù)的符號(hào)必須為右值引用符號(hào),即為&&
- 參數(shù)不可以是常量,因?yàn)楹瘮?shù)內(nèi)需要修改參數(shù)的值
- 參數(shù)的成員轉(zhuǎn)移后需要修改(如改為nullptr),避免臨時(shí)對(duì)象的析構(gòu)函數(shù)將資源釋放掉
為了方便我們理解,下面代碼包含了完整的移動(dòng)構(gòu)造和移動(dòng)運(yùn)算符,如下:
classBigObj{
public:
explicitBigObj(size_tlength)
:length_(length),data_(newint[length]){
}
//Destructor.
~BigObj(){
if(data_!=NULL){
delete[]data_;
length_=0;
}
}
//拷貝構(gòu)造函數(shù)
BigObj(constBigObj&other)
:length_(other.length_),data(newint[other.length_]){
std::copy(other.mData,other.mData+mLength,mData);
}
//賦值運(yùn)算符
BigObj&operator=(constBigObj&other){
if(this!=&other;){
delete[]data_;
length_=other.length_;
data_=newint[length_];
std::copy(other.data_,other.data_+length_,data_);
}
return*this;
}
//移動(dòng)構(gòu)造函數(shù)
BigObj(BigObj&&other):data_(nullptr),length_(0){
data_=other.data_;
length_=other.length_;
other.data_=nullptr;
other.length_=0;
}
//移動(dòng)賦值運(yùn)算符
BigObj&operator=(BigObj&&other){
if(this!=&other;){
delete[]data_;
data_=other.data_;
length_=other.length_;
other.data_=NULL;
other.length_=0;
}
return*this;
}
private:
size_tlength_;
int*data_;
};
intmain(){
std::vectorv;
v.push_back(BigObj(25));
v.push_back(BigObj(75));
v.insert(v.begin()+1,BigObj(50));
return0;
}
移動(dòng)構(gòu)造
移動(dòng)構(gòu)造函數(shù)的定義如下:
BigObj(BigObj&&other):data_(nullptr),length_(0){
data_=other.data_;
length_=other.length_;
other.data_=nullptr;
other.length_=0;
}
從上述代碼可以看出,它不分配任何新資源,也不會(huì)復(fù)制其它資源:other中的內(nèi)存被移動(dòng)
到新成員后,other中原有的內(nèi)容則消失了。換句話說(shuō),它竊取
了other的資源,然后將other設(shè)置為其默認(rèn)構(gòu)造的狀態(tài)。
在移動(dòng)構(gòu)造函數(shù)中,最最關(guān)鍵的一點(diǎn)是,它沒(méi)有額外的資源分配,僅僅是將其它對(duì)象的資源進(jìn)行了移動(dòng),占為己用。
在此,我們假設(shè)data_很大,包含了數(shù)百萬(wàn)個(gè)元素。如果使用原來(lái)拷貝構(gòu)造函數(shù)的話,就需要將該數(shù)百萬(wàn)元素挨個(gè)進(jìn)行復(fù)制,性能可想而知。而如果使用該移動(dòng)構(gòu)造函數(shù),因?yàn)椴簧婕暗叫沦Y源的創(chuàng)建,不僅可以節(jié)省很多資源,而且性能也有很大的提升。
移動(dòng)賦值運(yùn)算符
代碼如下:
BigObj&operator=(constBigObj&other){
if(this!=&other;){
delete[]data_;
length_=other.length_;
data_=newint[length_];
std::copy(other.data_,other.data_+length_,data_);
}
return*this;
}
移動(dòng)賦值運(yùn)算符的寫(xiě)法類似于拷貝賦值運(yùn)算符,所不同點(diǎn)在于:移動(dòng)賦值預(yù)算法會(huì)破壞被操作的對(duì)象(上述代碼中的參數(shù)other)。
移動(dòng)賦值運(yùn)算符的操作步驟如下:
- 釋放當(dāng)前擁有的資源
- 竊取他人資源
- 將他人資源設(shè)置為默認(rèn)狀態(tài)
- 返回*this
在定義移動(dòng)賦值運(yùn)算符的時(shí)候,需要進(jìn)行判斷,即被移動(dòng)的對(duì)象是否跟目標(biāo)對(duì)象一致,如果一致,則會(huì)出問(wèn)題,如下代碼:
data=std::move(data);
在上述代碼中,源和目標(biāo)是同一個(gè)對(duì)象,這可能會(huì)導(dǎo)致一個(gè)嚴(yán)重的問(wèn)題:它最終可能會(huì)釋放它試圖移動(dòng)的資源。為了避免此問(wèn)題,我們需要通過(guò)判斷來(lái)進(jìn)行,比如可以如下操作:
if(this==&other){
return*this
}
生成時(shí)機(jī)
眾所周知,在C++中有四個(gè)特殊的成員函數(shù):默認(rèn)構(gòu)造函數(shù)、析構(gòu)函數(shù),拷貝構(gòu)造函數(shù),拷貝賦值運(yùn)算符。
之所以稱之為特殊的成員函數(shù),這是因?yàn)槿绾伍_(kāi)發(fā)人員沒(méi)有定義這四個(gè)成員函數(shù),那么編譯器則在滿足某些特定條件(僅在需要的時(shí)候才生成,比如某個(gè)代碼使用它們但是它們沒(méi)有在類中明確聲明)下,自動(dòng)生成。這些由編譯器生成的特殊成員函數(shù)是public且inline。
自C++11起,引入了另外兩只特殊的成員函數(shù):移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符。如果開(kāi)發(fā)人員沒(méi)有顯示定義移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符,那么編譯器也會(huì)生成默認(rèn)。
與其他四個(gè)特殊成員函數(shù)不同,編譯器生成默認(rèn)的移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符需要,滿足以下條件:
- 如果一個(gè)類定義了自己的拷貝構(gòu)造函數(shù),拷貝賦值運(yùn)算符或者析構(gòu)函數(shù)(這三者之一,表示程序員要自己處理對(duì)象的復(fù)制或釋放問(wèn)題),編譯器就不會(huì)為它生成默認(rèn)的移動(dòng)構(gòu)造函數(shù)或者移動(dòng)賦值運(yùn)算符,這樣做的目的是防止編譯器生成的默認(rèn)移動(dòng)構(gòu)造函數(shù)或者移動(dòng)賦值運(yùn)算符不是開(kāi)發(fā)人員想要的
- 如果類中沒(méi)有提供移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符,且編譯器不會(huì)生成默認(rèn)的,那么我們?cè)诖a中通過(guò)std::move()調(diào)用的移動(dòng)構(gòu)造或者移動(dòng)賦值的行為將被轉(zhuǎn)換為調(diào)用拷貝構(gòu)造或者賦值運(yùn)算符
- 只有一個(gè)類沒(méi)有顯示定義拷貝構(gòu)造函數(shù)、賦值運(yùn)算符以及析構(gòu)函數(shù),且類的每個(gè)非靜態(tài)成員都可以移動(dòng)時(shí),編譯器才會(huì)生成默認(rèn)的移動(dòng)構(gòu)造函數(shù)或者移動(dòng)賦值運(yùn)算符
- 如果顯式聲明了移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值運(yùn)算符,則拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符將被隱式刪除(因此程開(kāi)發(fā)人員必須在需要時(shí)實(shí)現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符)
與拷貝操作一樣,如果開(kāi)發(fā)人員定義了移動(dòng)操作,那么編譯器就不會(huì)生成默認(rèn)的移動(dòng)操作,但是編譯器生成移動(dòng)操作的行為和生成拷貝操作的行為有些許不同,如下:
-
兩個(gè)拷貝操作是獨(dú)立的:聲明一個(gè)不會(huì)限制編譯器生成另一個(gè)。所以如果你聲明一個(gè)拷貝構(gòu)造函數(shù),但是沒(méi)有聲明拷貝賦值運(yùn)算符,如果寫(xiě)的代碼用到了拷貝賦值,編譯器會(huì)幫助你生成拷貝賦值運(yùn)算符。
同樣的,如果你聲明拷貝賦值運(yùn)算符但是沒(méi)有拷貝構(gòu)造函數(shù),代碼用到拷貝構(gòu)造函數(shù)時(shí)編譯器就會(huì)生成它。上述規(guī)則在C++98和C++11中都成立。
-
兩個(gè)移動(dòng)操作不是相互獨(dú)立的。如果你聲明了其中一個(gè),編譯器就不再生成另一個(gè)。如果你給類聲明了,比如,一個(gè)移動(dòng)構(gòu)造函數(shù),就表明對(duì)于移動(dòng)操作應(yīng)怎樣實(shí)現(xiàn),與編譯器應(yīng)生成的默認(rèn)逐成員移動(dòng)有些區(qū)別。
如果逐成員移動(dòng)構(gòu)造有些問(wèn)題,那么逐成員移動(dòng)賦值同樣也可能有問(wèn)題。所以聲明移動(dòng)構(gòu)造函數(shù)阻止編譯器生成移動(dòng)賦值運(yùn)算符,聲明移動(dòng)賦值運(yùn)算符同樣阻止編譯器生成移動(dòng)構(gòu)造函數(shù)。
類型轉(zhuǎn)換-move()函數(shù)
在前面的文章中,我們提到,如果需要調(diào)用移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符,就需要用到右值。那么,對(duì)于一個(gè)左值,又如何使用移動(dòng)語(yǔ)義呢?自C++11起,標(biāo)準(zhǔn)庫(kù)提供了一個(gè)函數(shù)move()用于將左值轉(zhuǎn)換成右值。
首先,我們看下cppreference中對(duì)move語(yǔ)義的定義:
std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object.
In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.
從上述描述,我們可以理解為std::move()
并沒(méi)有移動(dòng)
任何東西,它只是進(jìn)行類型轉(zhuǎn)換而已,真正進(jìn)行資源轉(zhuǎn)移的是開(kāi)發(fā)人員實(shí)現(xiàn)的移動(dòng)操作
。
該函數(shù)在STL中定義如下:
template<typename_Tp>
constexprtypenamestd::remove_reference<_Tp>::type&&
move(_Tp&&__t)noexcept
{returnstatic_cast<typenamestd::remove_reference<_Tp>::type&&>(__t);}
從上面定義可以看出,std::move()并不是什么黑魔法,而只是進(jìn)行了簡(jiǎn)單的類型轉(zhuǎn)換:
- 如果傳遞的是左值,則推導(dǎo)為左值引用,然后由static_cast轉(zhuǎn)換為右值引用
- 如果傳遞的是右值,則推導(dǎo)為右值引用,然后static_cast轉(zhuǎn)換為右值引用
使用move之后,就意味著兩點(diǎn):
- 原對(duì)象不再被使用,如果對(duì)其使用會(huì)造成不可預(yù)知的后果
- 所有權(quán)轉(zhuǎn)移,資源的所有權(quán)被轉(zhuǎn)移給新的對(duì)象
使用
在某些情況下,編譯器會(huì)嘗試隱式移動(dòng),這意味著您不必使用std::move()
。只有當(dāng)一個(gè)非常量的可移動(dòng)對(duì)象被傳遞、返回或賦值,并且即將被自動(dòng)銷毀時(shí),才會(huì)發(fā)生這種情況。
自c++11起,開(kāi)始支持右值引用。標(biāo)準(zhǔn)庫(kù)中很多容器都支持移動(dòng)語(yǔ)義,以std::vector<>為例,**vector::push_back()**定義了兩個(gè)重載版本,一個(gè)像以前一樣將const T&用于左值參數(shù),另一個(gè)將T&&類型的參數(shù)用于右值參數(shù)。如下代碼:
intmain(){
std::vectorv;
v.push_back(BigObj(10));
v.push_back(BigObj(20));
return0;
}
兩個(gè)push_back()調(diào)用都將解析為push_back(T&&),因?yàn)樗鼈兊膮?shù)是右值。push_back(T&&)使用BigObj的移動(dòng)構(gòu)造函數(shù)將資源從參數(shù)移動(dòng)到vector的內(nèi)部BigObj對(duì)象中。而在C++11之前,上述代碼則生成參數(shù)的拷貝,然后調(diào)用BigObj的拷貝構(gòu)造函數(shù)。
如果參數(shù)是左值,則將調(diào)用push_back(T&):
intmain(){
std::vectorv;
BigObjobj(10);
v.push_back(obj);//此處調(diào)用push_back(T&)
return0;
}
對(duì)于左值對(duì)象,如果我們想要避免拷貝操作,則可以使用標(biāo)準(zhǔn)庫(kù)提供的move()函數(shù)來(lái)實(shí)現(xiàn)(前提是類定義中實(shí)現(xiàn)了移動(dòng)語(yǔ)義),代碼如下:
intmain(){
std::vectorv;
BigObjobj(10);
v.push_back(std::move(obj));//此處調(diào)用push_back(T&&)
return0;
}
我們?cè)倏匆粋€(gè)常用的函數(shù)swap(),在使用移動(dòng)構(gòu)造之前,我們定義如下:
template
voidswap(T&a,T&b) {
Ttemp=a;//調(diào)用拷貝構(gòu)造函數(shù)
a=b;//調(diào)用operator=
b=temp;//調(diào)用operator=
}
如果T是簡(jiǎn)單類型,則上述轉(zhuǎn)換沒(méi)有問(wèn)題。但如果T是含有指針的復(fù)合數(shù)據(jù)類型,則上述轉(zhuǎn)換中會(huì)調(diào)用一次復(fù)制構(gòu)造函數(shù),兩次賦值運(yùn)算符重載。

而如果使用move()函數(shù)后,則代碼如下:
template
voidswap(T&a,T&b) {
Ttemp=std::move(a);
a=std::move(b);
b=std::move(temp);
}
與傳統(tǒng)的swap實(shí)現(xiàn)相比,使用move()函數(shù)的swap()版本減少了拷貝等操作。如果T是可移動(dòng)的,那么整個(gè)操作將非常高效。如果它是不可移動(dòng)的,那么它和普通的swap函數(shù)一樣,調(diào)用拷貝和賦值操作,不會(huì)出錯(cuò),且是安全可靠的。

經(jīng)驗(yàn)之談
對(duì)int等基礎(chǔ)類型進(jìn)行move()操作,不會(huì)改變其原值
對(duì)于所有的基礎(chǔ)類型-int、double、指針以及其它類型,它們本身不支持移動(dòng)操作(也可以說(shuō)本身沒(méi)有實(shí)現(xiàn)移動(dòng)語(yǔ)義,畢竟不屬于我們通常理解的對(duì)象嘛),所以,對(duì)于這些基礎(chǔ)類型進(jìn)行move()操作,最終還是會(huì)調(diào)用拷貝行為,代碼如下:
intmain()
{
inta=1;
int&&b=std::move(a);
std::cout<"a="<std::endl;
std::cout<"b="<std::endl;
return0;
}
最終結(jié)果輸出如下:
a=1
b=1
move構(gòu)造或者賦值函數(shù)中,請(qǐng)將原對(duì)象恢復(fù)默認(rèn)值
我們看如下代碼:
classBigObj{
public:
explicitBigObj(size_tlength)
:length_(length),data_(newint[length]){
}
//Destructor.
~BigObj(){
if(data_!=NULL){
delete[]data_;
length_=0;
}
}
//拷貝構(gòu)造函數(shù)
BigObj(constBigObj&other)=default;
//賦值運(yùn)算符
BigObj&operator=(constBigObj&other)=default;
//移動(dòng)構(gòu)造函數(shù)
BigObj(BigObj&&other):data_(nullptr),length_(0){
data_=other.data_;
length_=other.length_;
}
private:
size_tlength_;
int*data_;
};
intmain(){
BigObjobj(1000);
BigObjo;
{
o=std::move(obj);
}
//useobj;
return0;
}
在上述代碼中,調(diào)用移動(dòng)構(gòu)造函數(shù)后,沒(méi)有將原對(duì)象回復(fù)默認(rèn)值,導(dǎo)致目標(biāo)對(duì)象和原對(duì)象的底層資源(data_)執(zhí)行同一個(gè)內(nèi)存塊,這樣就導(dǎo)致退出main()函數(shù)的時(shí)候,原對(duì)象和目標(biāo)對(duì)象均調(diào)用析構(gòu)函數(shù)釋放同一個(gè)內(nèi)存塊,進(jìn)而導(dǎo)致程序崩潰。
不要在函數(shù)中使用std::move()進(jìn)行返回
我們?nèi)匀灰設(shè)bj進(jìn)行舉例,代碼如下:
Objfun(){
Objobj;
returnstd::move(obj);
}
intmain(){
Objo1=fun();
return0;
}
程序輸出:
inObj()0x7ffe600d79e0
inObj(constObj&&obj)
in~Obj()0x7ffe600d79e0
如果把fun()函數(shù)中的std::move(obj)換成return obj,則輸出如下:
inObj()0x7ffcfefaa750
通過(guò)上述示例的輸出,是不是有點(diǎn)超出我們的預(yù)期。從輸出可以看出來(lái),第二種方式(直接return obj)比第一種方式少了一次move構(gòu)造和析構(gòu)。這是因?yàn)榫幾g器做了NRVO優(yōu)化。
所以,我們需要切記:如果編譯器能夠?qū)δ硞€(gè)函數(shù)做(N)RVO優(yōu)化,就使用(N)RVO,而不是自作聰明使用std::move()。
知己知彼
STL中大部分已經(jīng)實(shí)現(xiàn)移動(dòng)語(yǔ)義,比如std::vector<>,std::map<>等,同時(shí)std::unique_ptr<>等不能被拷貝的類也支持移動(dòng)語(yǔ)義。
我們看下如下代碼:
classBigObj
{
public:
BigObj(){
std::cout<<__PRETTY_FUNCTION__<<std::endl;
}
~BigObj(){
std::cout<<__PRETTY_FUNCTION__<<std::endl;
}
BigObj(constBigObj&b){
std::cout<<__PRETTY_FUNCTION__<<std::endl;
}
BigObj(BigObj&&b){
std::cout<<__PRETTY_FUNCTION__<<std::endl;
}
};
intmain(){
std::arrayv;
autov1=std::move(v);
return0;
}
上述代碼輸出如下:
BigObj::BigObj()
BigObj::BigObj()
BigObj::BigObj(BigObj&&)
BigObj::BigObj(BigObj&&)
BigObj::~BigObj()
BigObj::~BigObj()
BigObj::~BigObj()
BigObj::~BigObj()
而如果把main()函數(shù)中的std::array<>換成std::vector<>后,如下:
intmain(){
std::vectorv;
v.resize(2);
autov1=std::move(v);
return0;
}
則輸出如下:
BigObj::BigObj()
BigObj::BigObj()
BigObj::~BigObj()
BigObj::~BigObj()
從上述兩處輸出可以看出,std::vector<>對(duì)應(yīng)的移動(dòng)構(gòu)造不會(huì)生成多余的構(gòu)造,且原本的element都移動(dòng)到v1中;而相比std::array<>中對(duì)應(yīng)的移動(dòng)構(gòu)造卻有很大的區(qū)別,基本上會(huì)對(duì)每個(gè)element都調(diào)用移動(dòng)構(gòu)造函數(shù)而不是對(duì)std::array<>本身。
因此,在使用std::move()的時(shí)候,最好要知道底層的基本實(shí)現(xiàn)原理,否則往往會(huì)得到我們意想不到的結(jié)果。
原文標(biāo)題:【Modern C++】深入理解移動(dòng)語(yǔ)義
文章出處:【微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
C++
+關(guān)注
關(guān)注
22文章
2119瀏覽量
75357 -
代碼
+關(guān)注
關(guān)注
30文章
4900瀏覽量
70790 -
語(yǔ)義
+關(guān)注
關(guān)注
0文章
21瀏覽量
8741
原文標(biāo)題:【Modern C++】深入理解移動(dòng)語(yǔ)義
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
一文詳解Linux C++內(nèi)存管理
一文初識(shí)C++

詳解C/C++中的getMemory()函數(shù)
C/C++軟件測(cè)試工具的元數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)與實(shí)現(xiàn)
C++ 語(yǔ)言命令詳解(第二版)
JAVA和C++區(qū)別詳解
EE-128:C++中的DSP:從C++調(diào)用匯編類成員函數(shù)

評(píng)論