一区二区三区三上|欧美在线视频五区|国产午夜无码在线观看视频|亚洲国产裸体网站|无码成年人影视|亚洲AV亚洲AV|成人开心激情五月|欧美性爱内射视频|超碰人人干人人上|一区二区无码三区亚洲人区久久精品

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文詳解C++中的移動(dòng)語(yǔ)義

Linux愛(ài)好者 ? 來(lái)源:高性能架構(gòu)探索 ? 作者:雨樂(lè) ? 2022-06-12 14:56 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

一直以來(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)容如下圖所示:

418232e6-ea04-11ec-ba43-dac502259ad0.png

值語(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)算符的操作步驟如下:

  1. 釋放當(dāng)前擁有的資源
  2. 竊取他人資源
  3. 將他人資源設(shè)置為默認(rèn)狀態(tài)
  4. 返回*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)算符重載。

41d44e78-ea04-11ec-ba43-dac502259ad0.png

而如果使用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ò),且是安全可靠的。

41f97ff4-ea04-11ec-ba43-dac502259ad0.png

經(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)注明出處。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • C++
    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)注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    詳解Linux C++內(nèi)存管理

    在互聯(lián)網(wǎng)的服務(wù),C++常用于搭建高性能、高并發(fā)、大流量、低延時(shí)的后端服務(wù)。如何合理的分配內(nèi)存滿足系統(tǒng)高性能需求是個(gè)高頻且重要的話題,而且因?yàn)閮?nèi)存自身的特點(diǎn)和實(shí)際問(wèn)題的復(fù)雜,組合出了諸多難題。
    發(fā)表于 10-25 12:02 ?964次閱讀

    初識(shí)C++

    后接-個(gè)或多個(gè)字符組成的。后綴告訴系統(tǒng)這個(gè)文件是個(gè)C++程序。不同編譯器使用不同的后綴命名約定,最常見(jiàn)的包括. cc、.cxx、.cpp、.cp及.C。
    發(fā)表于 07-17 15:14 ?386次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b>初識(shí)<b class='flag-5'>C++</b>

    詳解C/C++的getMemory()函數(shù)

    如果你將面試C/C++的工作,那么無(wú)論是筆試題或者面試題都有極大可能會(huì)被問(wèn)到getMemory()的問(wèn)題。當(dāng)然這也是道比較糾結(jié)的題目,本文就對(duì)這幾道題目來(lái)做
    發(fā)表于 07-17 17:35 ?1160次閱讀

    鴻蒙c++模板開(kāi)發(fā)詳解

    鴻蒙c++模板開(kāi)發(fā)詳解
    發(fā)表于 09-11 15:28

    C/C++軟件測(cè)試工具的元數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)與實(shí)現(xiàn)

    針對(duì)用C/C++語(yǔ)言進(jìn)行的語(yǔ)義分析,設(shè)計(jì)種中間結(jié)構(gòu),即元數(shù)據(jù)結(jié)構(gòu)。元數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)了源代碼的語(yǔ)義層次上的抽象,通過(guò)元數(shù)據(jù)結(jié)構(gòu)和相關(guān)應(yīng)用
    發(fā)表于 04-18 09:02 ?29次下載

    語(yǔ)義網(wǎng)詳解

    語(yǔ)義網(wǎng)詳解 1. 引言 2. 為什么需要語(yǔ)義網(wǎng)? 3. 
    發(fā)表于 08-04 10:33 ?2757次閱讀

    C++ 語(yǔ)言命令詳解(第二版)

    電子發(fā)燒友網(wǎng)站提供《C++ 語(yǔ)言命令詳解(第二版).txt》資料免費(fèi)下載
    發(fā)表于 07-28 13:06 ?0次下載

    JAVA和C++區(qū)別詳解

    1)java是解釋性語(yǔ)言,java程序在運(yùn)行時(shí)類加載器從類路經(jīng)中加載相關(guān)的類,然后java虛擬機(jī)讀取該類文件的字節(jié),執(zhí)行相應(yīng)操作.而C++編譯的 時(shí)候?qū)⒊绦蚓幾g成本地機(jī)器碼.般來(lái)說(shuō)java程序執(zhí)行
    發(fā)表于 12-01 09:12 ?527次閱讀

    圖文詳解C++虛表的剖析

    圖文詳解C++虛表的剖析
    的頭像 發(fā)表于 06-29 14:23 ?2802次閱讀
    圖文<b class='flag-5'>詳解</b>:<b class='flag-5'>C++</b>虛表的剖析

    圖文詳解C++的輸出輸入

    圖文詳解C++的輸出輸入
    的頭像 發(fā)表于 06-29 14:53 ?3645次閱讀
    圖文<b class='flag-5'>詳解</b>:<b class='flag-5'>C++</b>的輸出輸入

    EE-128:C++的DSP:從C++調(diào)用匯編類成員函數(shù)

    EE-128:C++的DSP:從C++調(diào)用匯編類成員函數(shù)
    發(fā)表于 04-16 17:04 ?2次下載
    EE-128:<b class='flag-5'>C++</b><b class='flag-5'>中</b>的DSP:從<b class='flag-5'>C++</b>調(diào)用匯編類成員函數(shù)

    C++mutable關(guān)鍵字詳解與實(shí)戰(zhàn)

    mutable關(guān)鍵字詳解與實(shí)戰(zhàn) 在C++mutable關(guān)鍵字是為了突破const關(guān)鍵字的限制,被mutable關(guān)鍵字修飾的成員變量永遠(yuǎn)處于可變的狀態(tài),即使是在被const修飾的成員函數(shù)
    的頭像 發(fā)表于 09-10 09:23 ?5785次閱讀

    嵌入式編程C語(yǔ)言到C++詳解

    ? OOP第C語(yǔ)言的局限 C++的特點(diǎn) C++的程序特征 C++程序的結(jié)構(gòu)特性 C++程序
    的頭像 發(fā)表于 11-08 17:21 ?2863次閱讀

    詳解C/C++堆棧的工作機(jī)制

    參數(shù),事實(shí)上是把參數(shù)壓入堆棧,聽(tīng)起來(lái),堆棧象個(gè)大雜燴。那么,堆棧(Stack)到底是如何工作的呢?本文將詳解C/C++堆棧的工作機(jī)制。閱讀時(shí)請(qǐng)注意以下幾點(diǎn):
    的頭像 發(fā)表于 07-29 09:09 ?1556次閱讀

    C++移動(dòng)語(yǔ)義介紹

    移動(dòng)語(yǔ)義是從C++11開(kāi)始引入的項(xiàng)全新功能。本文將為您撥開(kāi)云霧,讓您對(duì)移動(dòng)語(yǔ)義有個(gè)全面而深入的
    發(fā)表于 09-02 09:03 ?2017次閱讀