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

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

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

一文詳解C++中的移動語義

Linux愛好者 ? 來源:高性能架構(gòu)探索 ? 作者:雨樂 ? 2022-06-12 14:56 ? 次閱讀

一直以來,C++中基于值語義的拷貝和賦值嚴重影響了程序性能。尤其是對于資源密集型對象,如果進行大量的拷貝,勢必會對程序性能造成很大的影響。

為了盡可能的減小因為對象拷貝對程序的影響,開發(fā)人員使出了萬般招式:盡可能的使用指針、引用。而編譯器也沒閑著,通過使用RVO、NRVO以及復(fù)制省略技術(shù),來減小拷貝次數(shù)來提升代碼的運行效率。

但是,對于開發(fā)人員來說,使用指針和引用不能概括所有的場景,也就是說仍然存在拷貝賦值等行為;對于編譯器來說,而對于RVO、NRVO等編譯器行為的優(yōu)化需要滿足特定的條件(具體可以參考文章編譯器之返回值優(yōu)化)。

為了解決上述問題,自C++11起,引入了移動語義,更進一步對程序性能進行優(yōu)化 。

C++11新標準重新定義了lvalue和rvalue,并允許函數(shù)依照這兩種不同的類型進行重載。通過對于右值(rvalue)的重新定義,語言實現(xiàn)了移動語義(move semantics)和完美轉(zhuǎn)發(fā)(perfect forwarding),通過這種方法,C++實現(xiàn)了在保留原有的語法并不改動已存在的代碼的基礎(chǔ)上提升代碼性能的目的。

本文的主要內(nèi)容如下圖所示:

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

值語義

值語義(value semantics)指目標對象由源對象拷貝生成,且生成后與源對象完全無關(guān),彼此獨立存在,改變互不影響,就像int類型互相拷貝一樣。C++的內(nèi)置類型(bool/int/double/char)都是值語義,標準庫里的complex<> 、pair<>、vector<>、map<>、string等等類型也都是值語意,拷貝之后就與原對象脫離關(guān)系。

C++中基于值語義的拷貝構(gòu)造和賦值拷貝,會招致對資源密集型對象不必要拷貝,大量的拷貝很可能成為程序的性能瓶頸。

首先,我們看一段例子:

BigObjfun(BigObjobj){
BigObjo;
//dosth
returno;
}

intmain(){
fun(BigObj());
return0;
}

在上述代碼中,我們定義了一個函數(shù)fun()其參數(shù)是一個BigObj對象,當調(diào)用fun()函數(shù)時候,會通過調(diào)用BigObj的拷貝構(gòu)造函數(shù),將obj變量傳遞給fun()的參數(shù)。

編譯器知道何時調(diào)用拷貝構(gòu)造函數(shù)或者賦值運算符進行值傳遞。如果涉及到底層資源,比如內(nèi)存、socket等,開發(fā)人在定義類的時候,需要實現(xiàn)自己的拷貝構(gòu)造和賦值運算符以實現(xiàn)深拷貝。然而拷貝的代價很大,當我們使用STL容器的時候,都會涉及到大量的拷貝操作,而這些會浪費CPU和內(nèi)存等資源。

正如上述代碼中所示的那樣,當我們將一個臨時變量(BigObj(),也稱為右值)傳遞給一個函數(shù)的時候,就會導致拷貝操作,那么我們該如何避免此種拷貝行為呢?這就是我們本文的主題:移動語義

左值、右值

關(guān)于左值、右值,我們在之前的文章中已經(jīng)有過詳細的分享,有興趣的同學可以移步【Modern C++】深入理解左值、右值,在本節(jié),我們簡單介紹下左值和右值的概念,以方便理解下面的內(nèi)容。

左值(lvalue,left value),顧名思義就是賦值符號左邊的值。準確來說,左值是表達式結(jié)束(不一定是賦值表達式)后依然存在的對象。

可以將左值看作是一個關(guān)聯(lián)了名稱的內(nèi)存位置,允許程序的其他部分來訪問它。在這里,我們將 "名稱" 解釋為任何可用于訪問內(nèi)存位置的表達式。所以,如果 arr 是一個數(shù)組,那么 arr[1] 和 *(arr+1) 都將被視為相同內(nèi)存位置的“名稱”。

左值具有以下特征:

  • 可通過取地址運算符獲取其地址
  • 可修改的左值可用作內(nèi)建賦值和內(nèi)建符合賦值運算符的左操作數(shù)
  • 可以用來初始化左值引用(后面有講)

C++11將右值分為純右值將亡值兩種。純右值就是C++98標準中右值的概念,如非引用返回的函數(shù)返回的臨時變量值;一些運算表達式,如1+2產(chǎn)生的臨時變量;不跟對象關(guān)聯(lián)的字面量值,如2,'c',true,"hello";這些值都不能夠被取地址。

而將亡值則是C++11新增的和右值引用相關(guān)的表達式,這樣的表達式通常是將要移動的對象、T&&函數(shù)返回值、std::move()函數(shù)的返回值等。

左值引用、右值引用

在明確了左值和右值的概念之后,我們將在本節(jié)簡單介紹下左值引用和右值引用。

按照概念,對左值的引用稱為左值引用,而對右值的引用稱為右值引用。既然有了左值引用和右值引用,那么在C++11之前,我們通常所說的引用又是什么呢?其實就是左值引用,比如:

inta=1;
int&b=a;

在C++11之前,我們通過會說b是對a的一個引用(當然,在C++11及以后也可以這么說,大家潛移默化的認識就是引用==左值引用),但是在C++11中,更為精確的說法是b是一個左值引用。

在C++11中,為了區(qū)分左值引用,右值引用用&&來表示,如下:

int&&a=1;//a是一個左值引用
intb=1;
int&&c=b;//錯誤,右值引用不能綁定左值

跟左值引用一樣,右值引用不會發(fā)生拷貝,并且右值引用等號右邊必須是右值,如果是左值則會編譯出錯,當然這里也可以進行強制轉(zhuǎn)換,這將在后面提到。

在這里,有一個大家都經(jīng)常容易犯的一個錯誤,就是綁定右值的右值引用,其變量本身是個左值。為了便于理解,代碼如下:

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&,既可以綁定左值,又可以綁定右值,但是不能對其進行修改
  • 具名右值引用,編譯器會認為是個左值
  • 編譯器的優(yōu)化需要滿足特定條件,不能過度依賴

好了,截止到目前,相信你對左值引用和右值引用的概念有了初步的認識,那么,現(xiàn)在我們介紹下為什么要有右值引用呢?我們看下述代碼:

BigObjfun(){
returnBigObj();
}
BigObjobj=fun();//C++11以前
BigObj&&obj=fun();//C++11

上述代碼中,在C++11之前,我們只能通過編譯器優(yōu)化(N)RVO的方式來提升性能,如果不滿足編譯器的優(yōu)化條件,則只能通過拷貝等方式進行操作。自C++11引入右值引用后,對于不滿足(N)RVO條件,也可以通過避免拷貝延長臨時變量的生命周期,進而達到優(yōu)化的目的。

但是僅僅使用右值引用還不足以完全達到優(yōu)化目的,畢竟右值引用只能綁定右值。那么,對于左值,我們又該如何優(yōu)化呢?是否可以通過左值轉(zhuǎn)成右值,然后進行優(yōu)化呢?等等

為了解決上述問題,標準引入了移動語義。通移動語義,可以在必要的時候避免拷貝;標準提供了move()函數(shù),可以將左值轉(zhuǎn)換成右值。接下來,就開始我們本文的重點-移動語義。

移動語義

移動語義是Howard Hinnant在2002年向C++標準委員會提議的,引用其在移動語義提案上的一句話:

移動語義不是試圖取代復(fù)制語義,也不是以任何方式破壞它。相反,該提議旨在增強復(fù)制語義

對于剛剛接觸移動語義的開發(fā)人員來說,很難理解為什么有了值語義還需要有移動語義。

我們可以想象一下,有一輛汽車,在內(nèi)置發(fā)動機的情況下運行平穩(wěn),有一天,在這輛車上安裝了一個額外的V8發(fā)動機。當有足夠燃料的時候,V8發(fā)動機就能進行加速。所以,汽車是值語義,而V8引擎則是移動語義。在車上安裝引擎不需要一輛新車,它仍然是同一輛車,就像移動語義不會放棄值語義一樣。

所以,如果可以,使用移動語義,否則使用值語義,換句話說就是,如果燃料充足,則使用V8引擎,否則使用原始默認引擎。

好了,截止到現(xiàn)在,我們對移動語義有一個感官上的認識,它屬于一種優(yōu)化,或者說屬于錦上添花。再次引用Howard Hinnant在移動語義提案上的一句話:

移動語義主要是性能優(yōu)化:將昂貴的對象從內(nèi)存中的一個地址移動到另外一個地址的能力,同時竊取源資源以便以最小的代價構(gòu)建目標

在C++11之前,當進行值傳遞時,編譯器會隱式調(diào)用拷貝構(gòu)造函數(shù);自C++11起,通過右值引用來避免由于拷貝調(diào)用而導致的性能損失。

右值引用的主要用途是創(chuàng)建移動構(gòu)造函數(shù)和移動賦值運算符。移動構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù)一樣,將對象的實例作為其參數(shù),并從原始對象創(chuàng)建一個新的實例。

但是,移動構(gòu)造函數(shù)可以避免內(nèi)存重新分配,這是因為移動構(gòu)造函數(shù)的參數(shù)是一個右值引用,也可以說是一個臨時對象,而臨時對象在調(diào)用之后就被銷毀不再被使用,因此,在移動構(gòu)造函數(shù)中對參數(shù)進行移動而不是拷貝。換句話說,右值引用移動語義允許我們在使用臨時對象時避免不必要的拷貝。

移動語義通過移動構(gòu)造函數(shù)移動賦值操作符實現(xiàn),其與拷貝構(gòu)造函數(shù)類似,區(qū)別如下:

  • 參數(shù)的符號必須為右值引用符號,即為&&
  • 參數(shù)不可以是常量,因為函數(shù)內(nèi)需要修改參數(shù)的值
  • 參數(shù)的成員轉(zhuǎn)移后需要修改(如改為nullptr),避免臨時對象的析構(gòu)函數(shù)將資源釋放掉

為了方便我們理解,下面代碼包含了完整的移動構(gòu)造和移動運算符,如下:

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);
}

//賦值運算符
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;
}

//移動構(gòu)造函數(shù)
BigObj(BigObj&&other):data_(nullptr),length_(0){
data_=other.data_;
length_=other.length_;

other.data_=nullptr;
other.length_=0;
}

//移動賦值運算符
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;
}

移動構(gòu)造

移動構(gòu)造函數(shù)的定義如下:

BigObj(BigObj&&other):data_(nullptr),length_(0){
data_=other.data_;
length_=other.length_;

other.data_=nullptr;
other.length_=0;
}

從上述代碼可以看出,它不分配任何新資源,也不會復(fù)制其它資源:other中的內(nèi)存被移動到新成員后,other中原有的內(nèi)容則消失了。換句話說,它竊取了other的資源,然后將other設(shè)置為其默認構(gòu)造的狀態(tài)。

在移動構(gòu)造函數(shù)中,最最關(guān)鍵的一點是,它沒有額外的資源分配,僅僅是將其它對象的資源進行了移動,占為己用。

在此,我們假設(shè)data_很大,包含了數(shù)百萬個元素。如果使用原來拷貝構(gòu)造函數(shù)的話,就需要將該數(shù)百萬元素挨個進行復(fù)制,性能可想而知。而如果使用該移動構(gòu)造函數(shù),因為不涉及到新資源的創(chuàng)建,不僅可以節(jié)省很多資源,而且性能也有很大的提升。

移動賦值運算符

代碼如下:

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;
}

移動賦值運算符的寫法類似于拷貝賦值運算符,所不同點在于:移動賦值預(yù)算法會破壞被操作的對象(上述代碼中的參數(shù)other)。

移動賦值運算符的操作步驟如下:

  1. 釋放當前擁有的資源
  2. 竊取他人資源
  3. 將他人資源設(shè)置為默認狀態(tài)
  4. 返回*this

在定義移動賦值運算符的時候,需要進行判斷,即被移動的對象是否跟目標對象一致,如果一致,則會出問題,如下代碼:

data=std::move(data);

在上述代碼中,源和目標是同一個對象,這可能會導致一個嚴重的問題:它最終可能會釋放它試圖移動的資源。為了避免此問題,我們需要通過判斷來進行,比如可以如下操作:

if(this==&other){
return*this
}

生成時機

眾所周知,在C++中有四個特殊的成員函數(shù):默認構(gòu)造函數(shù)、析構(gòu)函數(shù),拷貝構(gòu)造函數(shù),拷貝賦值運算符。

之所以稱之為特殊的成員函數(shù),這是因為如何開發(fā)人員沒有定義這四個成員函數(shù),那么編譯器則在滿足某些特定條件(僅在需要的時候才生成,比如某個代碼使用它們但是它們沒有在類中明確聲明)下,自動生成。這些由編譯器生成的特殊成員函數(shù)是public且inline。

自C++11起,引入了另外兩只特殊的成員函數(shù):移動構(gòu)造函數(shù)和移動賦值運算符。如果開發(fā)人員沒有顯示定義移動構(gòu)造函數(shù)和移動賦值運算符,那么編譯器也會生成默認。

與其他四個特殊成員函數(shù)不同,編譯器生成默認的移動構(gòu)造函數(shù)和移動賦值運算符需要,滿足以下條件:

  • 如果一個類定義了自己的拷貝構(gòu)造函數(shù),拷貝賦值運算符或者析構(gòu)函數(shù)(這三者之一,表示程序員要自己處理對象的復(fù)制或釋放問題),編譯器就不會為它生成默認的移動構(gòu)造函數(shù)或者移動賦值運算符,這樣做的目的是防止編譯器生成的默認移動構(gòu)造函數(shù)或者移動賦值運算符不是開發(fā)人員想要的
  • 如果類中沒有提供移動構(gòu)造函數(shù)和移動賦值運算符,且編譯器不會生成默認的,那么我們在代碼中通過std::move()調(diào)用的移動構(gòu)造或者移動賦值的行為將被轉(zhuǎn)換為調(diào)用拷貝構(gòu)造或者賦值運算符
  • 只有一個類沒有顯示定義拷貝構(gòu)造函數(shù)、賦值運算符以及析構(gòu)函數(shù),且類的每個非靜態(tài)成員都可以移動時,編譯器才會生成默認的移動構(gòu)造函數(shù)或者移動賦值運算符
  • 如果顯式聲明了移動構(gòu)造函數(shù)或移動賦值運算符,則拷貝構(gòu)造函數(shù)和拷貝賦值運算符將被隱式刪除(因此程開發(fā)人員必須在需要時實現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值運算符)

與拷貝操作一樣,如果開發(fā)人員定義了移動操作,那么編譯器就不會生成默認的移動操作,但是編譯器生成移動操作的行為和生成拷貝操作的行為有些許不同,如下:

  • 兩個拷貝操作是獨立的:聲明一個不會限制編譯器生成另一個。所以如果你聲明一個拷貝構(gòu)造函數(shù),但是沒有聲明拷貝賦值運算符,如果寫的代碼用到了拷貝賦值,編譯器會幫助你生成拷貝賦值運算符。

    同樣的,如果你聲明拷貝賦值運算符但是沒有拷貝構(gòu)造函數(shù),代碼用到拷貝構(gòu)造函數(shù)時編譯器就會生成它。上述規(guī)則在C++98和C++11中都成立。
  • 兩個移動操作不是相互獨立的。如果你聲明了其中一個,編譯器就不再生成另一個。如果你給類聲明了,比如,一個移動構(gòu)造函數(shù),就表明對于移動操作應(yīng)怎樣實現(xiàn),與編譯器應(yīng)生成的默認逐成員移動有些區(qū)別。

    如果逐成員移動構(gòu)造有些問題,那么逐成員移動賦值同樣也可能有問題。所以聲明移動構(gòu)造函數(shù)阻止編譯器生成移動賦值運算符,聲明移動賦值運算符同樣阻止編譯器生成移動構(gòu)造函數(shù)。

類型轉(zhuǎn)換-move()函數(shù)

在前面的文章中,我們提到,如果需要調(diào)用移動構(gòu)造函數(shù)和移動賦值運算符,就需要用到右值。那么,對于一個左值,又如何使用移動語義呢?自C++11起,標準庫提供了一個函數(shù)move()用于將左值轉(zhuǎn)換成右值。

首先,我們看下cppreference中對move語義的定義:

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()并沒有移動任何東西,它只是進行類型轉(zhuǎn)換而已,真正進行資源轉(zhuǎn)移的是開發(fā)人員實現(xiàn)的移動操作

該函數(shù)在STL中定義如下:

template<typename_Tp>
constexprtypenamestd::remove_reference<_Tp>::type&&
move(_Tp&&__t)noexcept
{returnstatic_cast<typenamestd::remove_reference<_Tp>::type&&>(__t);}

從上面定義可以看出,std::move()并不是什么黑魔法,而只是進行了簡單的類型轉(zhuǎn)換:

  • 如果傳遞的是左值,則推導為左值引用,然后由static_cast轉(zhuǎn)換為右值引用
  • 如果傳遞的是右值,則推導為右值引用,然后static_cast轉(zhuǎn)換為右值引用

使用move之后,就意味著兩點:

  • 原對象不再被使用,如果對其使用會造成不可預(yù)知的后果
  • 所有權(quán)轉(zhuǎn)移,資源的所有權(quán)被轉(zhuǎn)移給新的對象

使用

在某些情況下,編譯器會嘗試隱式移動,這意味著您不必使用std::move()。只有當一個非常量的可移動對象被傳遞、返回或賦值,并且即將被自動銷毀時,才會發(fā)生這種情況。

自c++11起,開始支持右值引用。標準庫中很多容器都支持移動語義,以std::vector<>為例,**vector::push_back()**定義了兩個重載版本,一個像以前一樣將const T&用于左值參數(shù),另一個將T&&類型的參數(shù)用于右值參數(shù)。如下代碼:

intmain(){
std::vectorv;
v.push_back(BigObj(10));
v.push_back(BigObj(20));

return0;
}

兩個push_back()調(diào)用都將解析為push_back(T&&),因為它們的參數(shù)是右值。push_back(T&&)使用BigObj的移動構(gòu)造函數(shù)將資源從參數(shù)移動到vector的內(nèi)部BigObj對象中。而在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;
}

對于左值對象,如果我們想要避免拷貝操作,則可以使用標準庫提供的move()函數(shù)來實現(xiàn)(前提是類定義中實現(xiàn)了移動語義),代碼如下:

intmain(){
std::vectorv;
BigObjobj(10);
v.push_back(std::move(obj));//此處調(diào)用push_back(T&&)

return0;
}

我們再看一個常用的函數(shù)swap(),在使用移動構(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是簡單類型,則上述轉(zhuǎn)換沒有問題。但如果T是含有指針的復(fù)合數(shù)據(jù)類型,則上述轉(zhuǎn)換中會調(diào)用一次復(fù)制構(gòu)造函數(shù),兩次賦值運算符重載。

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實現(xiàn)相比,使用move()函數(shù)的swap()版本減少了拷貝等操作。如果T是可移動的,那么整個操作將非常高效。如果它是不可移動的,那么它和普通的swap函數(shù)一樣,調(diào)用拷貝和賦值操作,不會出錯,且是安全可靠的。

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

經(jīng)驗之談

對int等基礎(chǔ)類型進行move()操作,不會改變其原值

對于所有的基礎(chǔ)類型-int、double、指針以及其它類型,它們本身不支持移動操作(也可以說本身沒有實現(xiàn)移動語義,畢竟不屬于我們通常理解的對象嘛),所以,對于這些基礎(chǔ)類型進行move()操作,最終還是會調(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ù)中,請將原對象恢復(fù)默認值

我們看如下代碼:

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;

//賦值運算符
BigObj&operator=(constBigObj&other)=default;

//移動構(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)用移動構(gòu)造函數(shù)后,沒有將原對象回復(fù)默認值,導致目標對象和原對象的底層資源(data_)執(zhí)行同一個內(nèi)存塊,這樣就導致退出main()函數(shù)的時候,原對象和目標對象均調(diào)用析構(gòu)函數(shù)釋放同一個內(nèi)存塊,進而導致程序崩潰。

不要在函數(shù)中使用std::move()進行返回

我們?nèi)匀灰設(shè)bj進行舉例,代碼如下:

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

通過上述示例的輸出,是不是有點超出我們的預(yù)期。從輸出可以看出來,第二種方式(直接return obj)比第一種方式少了一次move構(gòu)造和析構(gòu)。這是因為編譯器做了NRVO優(yōu)化。

所以,我們需要切記:如果編譯器能夠?qū)δ硞€函數(shù)做(N)RVO優(yōu)化,就使用(N)RVO,而不是自作聰明使用std::move()。

知己知彼

STL中大部分已經(jīng)實現(xiàn)移動語義,比如std::vector<>,std::map<>等,同時std::unique_ptr<>等不能被拷貝的類也支持移動語義。

我們看下如下代碼:

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<>對應(yīng)的移動構(gòu)造不會生成多余的構(gòu)造,且原本的element都移動到v1中;而相比std::array<>中對應(yīng)的移動構(gòu)造卻有很大的區(qū)別,基本上會對每個element都調(diào)用移動構(gòu)造函數(shù)而不是對std::array<>本身。

因此,在使用std::move()的時候,最好要知道底層的基本實現(xiàn)原理,否則往往會得到我們意想不到的結(jié)果。

原文標題:【Modern C++】深入理解移動語義

文章出處:【微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

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

    關(guān)注

    21

    文章

    2102

    瀏覽量

    73457
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4728

    瀏覽量

    68250
  • 語義
    +關(guān)注

    關(guān)注

    0

    文章

    21

    瀏覽量

    8656

原文標題:【Modern C++】深入理解移動語義

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

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

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

    初識C++

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

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

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

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

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

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

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

    語義網(wǎng)詳解

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

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

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

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

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

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

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

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

    圖文詳解C++的輸出輸入
    的頭像 發(fā)表于 06-29 14:53 ?3354次閱讀
    圖文<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)鍵字詳解與實戰(zhàn)

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

    嵌入式編程C語言到C++詳解

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

    詳解C/C++堆棧的工作機制

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

    C++移動語義介紹

    移動語義是從C++11開始引入的項全新功能。本文將為您撥開云霧,讓您對移動語義有個全面而深入的
    發(fā)表于 09-02 09:03 ?1796次閱讀