C++ 中的「移動」在內存或者寄存器中的操作是什麼,為什麼就比拷貝賦值性能高呢?
是類似位操作的左移右移嗎?
拷貝,指的是經過拷貝操作後,源和目標值一樣。
移動,則是目標有了源的值,源是什麼無所謂。所以,對於基本類型來說,兩者其實一樣,都是拷貝。但對於有些特定類型來說,比如vector,那麼移動就只需要把指針和大小等幾個位元組的東西給過去,而不用拷貝vector的內容。因為移動過後源有權掛了。而這些都是在移動構造裡面你自己寫的。移動複製構造函數也是你自己寫的(不寫的話,編譯器會在合適的時候替你寫)一個普通的函數而已。一個類的業務怎麼定義,他移動的時候就要做什麼操作。這沒有什麼魔法。
總的來說,從A移動到B,B要做的事情就是,把A的身體掏空,放在自己裡面。這個時候你假定A不會再被使用了,因此A的下一個操作必然是析構函數。所以你在掏空A的身體的時候,要讓析構函數可以正確運行(所以就會有什麼指針拿走之後要賦值成nullptr的這些做法,方便析構函數編寫)。然後你的移動複製構造函數就搞定了。
編譯器在複製對象的時候,會在不同的情況看一下被複制的東西是不是以後不會有人用了,如果是的話就會調用移動複製構造函數。你也可以通過std::move來強行讓編譯器在這種情況下調用它。其實是一種淺拷貝
很多人都愛說什麼,移動啊,掏空啊這些,我估計新人是很難理解的。
我覺得還是用c++03的swap效果說明更好吧。
對於std::string, in c++ 03, c=a+b 在賦值給c的時候有一次拷貝操作。
不過我們可以通過(a+b).swap(c) 來避免(交換是輕量的!string底層存儲是pimpl實現,只需要交換指針即可)。
in c++11, 我們可以直接c=a+b。編譯器能讓賦值函數知道a+b的結果除了c需要引用別的地方都沒機會用到了,那就直接把a+b的結果"移動"給c就行了唄(把c的指針銷毀,把a+b的指針給c, 再把a+b的指針賦值為空否則會2個人同時持有1個指針。 比(a+b).swap(c)其實也省不到哪裡去,但是語法上自然的太多)。
重要tips:移動的是對象持有的指針,被移動者需要得到合理的善後。
"C++中的右值移動,"移動"在內存或者寄存器中的操作究竟是什麼?"
這個問題裡面的"右值移動",我沒有印象,一般看到的說法是:右值引用(rvalue reference),移動語義(move semantics)。
我覺得,是不是想問的是:"C++中,在右值引用,移動語義部分,提到了好多"移動",這些"移動",是什麼意思?"。。
因為確實有好多地方提到了"移動",而且我對"右值移動"這個說法也沒有印象,所以我把問題理解成這個來回答吧。其實最好是把看到的原文一起貼上來,然後把提到"移動"的句子標出,然後才好說。
雖然對"右值移動"沒有印象,但是我對"移動一個值"這個說法卻是有印象的,可能你想問的是這個?還有就是,std::move(),這個函數名字就叫move。。他是要移動什麼東西嗎?
總結一下,是兩個問題:
1&>移動一個值是什麼意思?
2&>std::move()中的move是什麼意思,是不是真的在移動什麼?
下面的內容是這本書上的:The C++ programming language / Bjarne Stroustrup.—Fourth edition.
(以下截圖都不是連續的,都是跳著截的)
1&>移動一個值是什麼意思?
大概意思是:你看,在這個swap函數裡面,我們根本就不想,這樣複製過去,複製過來,太浪費了。然後他打了個比方來說明怎麼解決這個問題:如果你借我電話,我把電話給你就好了,而不是複製一個我的電話,然後再拿給你。如果你借我車,我把鑰匙給你,也不是重新造個車給你。。一旦我給你了我的東西,我就沒啦,你就有了,比方說,我把電話給你,你拿去打電話了,這個時候我手上就沒有電話了。
所以說,我們這裡說的是:轉移所有權,"移動"物理對象。
請看原文中提到的幾個詞:"giving away","handing over","transferring ownership","moving" physical objects。
這就是"移動(move)"的語境。"giving away","handing over",翻譯過來大概就是分發,遞給,總之就是,上面提到的:打電話給電話,開車給鑰匙,這種避免複製,直接給的做法。
然後他緊接著就說:計算機中的很多對象更像物理對象(能不複製就不複製)...
剛剛我們解決了物理對象的問題,然後現在又說計算機中的對象像物理對象,那麼意思就是:在計算機裡面,比如上面說的swap函數裡面的情況,我們不想複製,就好比上面打比方中的例子一樣,也是不想複製,那怎麼辦呢?也是轉移所有權,直接給,像"移動"物理對象那樣去做。
然後又是一段:
大概意思是:就是為了避免這種很浪費的複製,C++同時支持:the notion of moving和the notion of copying。
所以,在這個語境下,"移動一個值是什麼意思?"。其實意思和打的比方中的例子一樣,"移動"是一種避免浪費的方法:避免複製,直接給(我自己的理解。。)。那麼,在C++中,什麼時候會遇到像別人借電話,借車這種情況,需要我們這樣"移動"對象,來避免浪費呢?swap中我們就需要這樣做,但是我覺得用另一個例子能說得更清楚。
(到目前為止,我們一直說的都是:想要這樣做,而沒有說,怎樣去做。也就是說,這種"移動"是怎麼實現的?在這個語境下,我們說的"移動",這種避免浪費的方法到底如何實施?可以暫時不管,先看什麼情況下需要這樣做)
這個例子在3.3.2 Moving Containers部分:
看最後一句,他說:"我們想移動Vector而不是複製它"。這就是別人借電話,借車的場景了!
他例子裡面是r=x+y+z,我們看簡單一點的,就一個加號的情況:
比如:
Vector c;
...
c=a+b;
...
c=a+b發生了什麼?首先是那個+運算符,局部變數res記錄了a+b的和的信息,然後那個函數就要返回了,此時生成一個Vector,假設叫temp,然後把res中的內容全部拷貝到temp(沒有優化的話,有優化的話我不知道是什麼情況了。。)。然後就是=運算符了,temp中的內容又要複製一遍到c。也就是說,在這個過程中複製了2次,這就造成了浪費了。
所以說,回想剛才類比的情況,我們想怎麼做呢?避免兩次複製,使得Vector c中的內容就是原來res中的內容。這就是問題1&>"移動一個值是什麼意思"的答案,要在具體例子,具體語境裡面才能說清楚。
下面就是看,怎麼實施這種避免浪費的方法(移動)了。
也就是說,需要做的是:
1.添加move constructor和move assignment
2.使用std::move() //有時候需要,有時候不需要
先看1.:
他說了move constructor具體怎麼定義:這應該就是你問的,具體操作是什麼了,可以看到他是複製了一下指針,也就是說,函數返回時,我們假設的,會生成的那個temp,就不是把res[0],res[1],res[2]...全部拷貝一遍,而是直接把它的數據成員elem,把這個指針拷貝了,這也有一點轉移所有權的意味。
另外,move assignment也需要自己合理定義。還有就是,我覺得,這些函數裡面的內容是自己寫的,如果你不是複製指針,而是還像copy constructor中那樣,複製那些數組中的值,就達不到你的目的,但是應該還是不會報錯的。
那麼,這個語境下,""移動"在內存或者寄存器中的操作究竟是什麼?",操作就是上面move constructor,move assignment中寫的內容。
再看2.:
大概意思就是說:編譯器怎麼知道什麼時候是移動(調用move constructor),什麼時候是複製(調用copy constructor)?少數情況,比如返回值的時候,編譯器自己知道選(舉了個Matrix的例子),但是,一般情況下我們要告訴它,怎麼告訴呢?通過提供一個類型為右值引用(rvalue reference)的參數來告訴它。
然後舉了個swap的例子,這裡就出現了std::move(),這就是問題2&>中要說的了。
2&>std::move()中的move是什麼意思,是不是真的在移動什麼?
這一段大概意思就是:move()是一個標準庫函數,返回一個右值引用,move(x)的意思是:給我一個x的右值引用。也就是說std::move(x)沒有移動什麼東西,但是它使得我們可以去移動x。(這裡說的移動,就是問題1&>那個語境中說的移動:一種避免浪費的方法。)如果move()叫rval()的話,會更好,但是move()已經用了很多年了。
後面也有提到這個命名的,大概意思就是:我也認為move()本來應該叫rvalue()的,因為實際上它什麼東西也沒有移動。然後,這本書的作者是Bjarne Stroustrup。。
那麼總結一下就是:比如在swap函數裡面:
我們想要避免複製,想要調用move assignment,怎麼辦呢?
辦法是:提醒編譯器,調用move assignment。
怎麼提醒呢?
辦法是:提供a的右值引用作為move assignment的參數。
但是,怎麼才能得到a的右值引用呢?
辦法是:使用std::move()返回a的右值引用。
剩下的事情就都是編譯器來幫忙完成了,當然,前提是已經寫好了move constructor,move assignment。
大致的意思:左值是一個「=」的賦值目標,右值是要賦過去的那個值。這裡的左右是相對於那個等號來講的。
請問右值的「移動」在內存或者寄存器中究竟指的是什麼啊,是類似位操作的左移右移嗎?
你無法說「它在內存、寄存器中究竟是什麼」,因為C++是一個帶包裝的語言,賦值表達式的內容是可以自定義的。同樣的理由,這裡的「移動」完全是概念上的,可以對應任何實際操作。
你不要從純的概念上入手,其實它是基於這樣一種實用的目的:為臨時對象提供輕量的複製方法。比如對於一個列表對象,如果是正式的複製,那麼就是:
- 在目標列表裡分配一樣大的內存。
- 將源列表裡的每個值都「拷貝」到目標列表裡。注意這個代價可能非常高昂,比如我列表裡存的不是整數一類的基本類型,而是我自定的對象,每次對象複製都要帶動裡面的一大坨東西。
現在引入了「右值引用」和「移動複製」的概念。我可以實現這樣一套輕量的函數:
- 將源列表的資源塞給目標列表。
- 將源列表標記為空。
這樣的操作顯然輕了很多。然後,當編譯器認為某個對象「只是個右值」,就會觸發這種輕量的移動複製。
移動語義僅僅是一個「語義」,編譯出來之後就體現不出來了
移動語義體現在一個即將被銷毀的對象,可以直接將它所持有的資源的所有權轉移給一個別的對象;或者給你一種機制,能顯式的轉移對象所持有的資源的所有權給別的對象。
在有移動語義之前,如果你想用一個函數返回的vector(即將被銷毀的對象)來賦值一個變數,就像vector&
在有了移動語義之後,只需要把消亡值內持有的內存的指針轉交給被初始化的對象就好了,省了一次開闢空間一次拷貝和一次delete。
大概題主沒明白什麼是「資源所有權」,再過幾周就明白了,我當初學也是這樣一頭霧水。
std::move本身沒有什麼神秘,實際上也並沒有move任何東西,std::move的實現大致是這樣:
template&
inline typename std::remove_reference&<_Tp&>::type
move(_Tp __t)
{
return static_cast&
}
std::move只是做了一個cast而已,將參數轉化為右值(rvalue),這個cast的目的就是用來觸發移動語義也就是move semantics,讓編譯器調用移動構造函數或者移動賦值運算符,來執行移動操作。
C++11引入std::move主要的目的就是為了減少拷貝操作,來獲得性能上的提升。這主要是通過指針操作代替拷貝賦值。A = std::move(B),這裡編譯器調用了A的移動拷貝運算符,把指向B的指針給A,再將B置空,用指針賦值操作來代替拷貝賦值,對於體積比較龐大的數據結構來說,顯然是一種更高效的方法,因為move操作只需要O(1)時間,而copy操作則需要O(n)的時間。
舉個例子直觀一點,將vector B賦值給另一個vector A,如果是拷貝賦值,那麼顯然要對B中的每一個元素執行一個copy操作到A,如果是移動賦值的話,只需要將指向B的指針拷貝到A中即可,試想一下如果vector中有相當多的元素,那是不是用move來代替copy就顯得十分高效了呢?
建議看一看Scott Meyers 的Effective Modern C++,裡面對移動語義、右值引用以及類型推導進行了深入的探索單純的右值(不包括通用引用)和轉移語意的話,實際上什麼也沒幹,就換了一下類型而已,基本上就是語法糖。
其實這個改變類型是給編譯器看得,因為重載函數的匹配是根據參數來的。具體到某個STL容器,其重載的構造函數如果參數是右值的話一般會執行swap操作,而不是copy操作。
就醬是邏輯上的移動,跟底層沒關係。
某些情況下源對象賦值給另外的對象之後就沒用了,這個時候使用移動可以將對象包含的資源移動,減少資源的重新分配。
移動其實沒有什麼特別的,只是用於另一種特殊情景的複製函數(移動構造函數)就可以了。至於移動的效率是根據你寫的移動構造函數或者編譯器生成的移動構造函數決定的,背後並沒有特殊的魔法,還是普通的函數調用罷了。移動效率之所以有時候比複製快,是因為移動不需要保留源對象的值,因此在實現上可以做一些優化。
不廢話了,上圖
這是複製構造,實際實現的時候差不多相當於跑一個for循環,時間複雜度是O(n)右值引用,這是直接把指針拿過來(看著像淺複製)原來的指針設置成nullptr原來的對象被析構,結束——可以看到時間複雜度只有O(1),也就是光是把指針拿過來這一個操作而已。以前的時候,函數返回一個對象,需要先把這個對象複製出來一個,然後再把舊的對象銷毀,等於是先複製一塊堆數據,再把原來的堆數據炸掉——現在有右值引用的話,直接把原來那個對象的堆數據拿過來用就行了,這樣可以節省巨量的時間開銷。在Windows下,在同一個分區內,把一個文件先複製到另一個文件夾,然後再刪除原來那個文件,這相當於是以前的返回值方式;而直接將文件剪切過去,這個相當於右值引用。毫無疑問,前者需要跟文件大小成正比的時間,而後者可以瞬間完成……C++的move根本就是騙人的,畢竟是右值引用,而不是右值,是待在原地不動的,都還要調用destructor的,怎麼能說是move?講道理,Rust那才是真的move。
move並不能減少複製,C++引入move僅僅是為了能表達獨佔。沒有move語義,只有copy語義,會導致很多沒必要使用引用計數的地方被迫使用引用計數,或者某種變相的引用計數。沒有move(和using),在C++里,連allocator都不可能搞對。
我想起來,侯捷有一本著名的深入淺出MFC。第一章標題好像是勿在浮沙築高台?然而C++和MFC,哪個不是在流沙上建起來摩天大樓?不一定比拷貝更快。
平常的c++拷貝,是把原始內容複製一遍再清除原數據。
打個比方吧,你先把你滿滿一個硬碟的電影給你的朋友,原先c++做法是買一個新硬碟給他,把硬碟裡面的文件一個一個拷給他,再把你的硬碟給銷毀掉。既然你的硬碟最後要被銷毀,那乾脆直接送給你朋友不就好了嗎?右值移動構造,就是這麼個意思。體現在代碼上,可以說就是把指針「複製」了一下,把自己擁有的內存,直接送給了別人。但是對於棧上的那些數據,move是沒意義的。堆上的東西,大家送來送去沒關係。棧上的東西卻是早早規劃好的,包分配,送不得。
理解move其實不太難,但是要先把平時copy了解了才行。std:move本身只是簡單地將輸入參數cast成右值引用,這一步的操作並沒有什麼「實質」意義。C++移動語義的關鍵在於把std:move的返回值-右值引用-傳遞給一個移動構造函數的時候,移動構造函數裡面簡單執行指針拷貝,而不像拷貝構造那樣需要要分配新的內存空間,所以效率得到了提高。還有一個關鍵點就是,被std:move後傳遞給移動構造函數的那個對象不能再用了-因為它內部指針已經空了。
建議樓主好好研究一下c++11標準中的右值。
具體實現看靠移動構造 /賦值函數標準用法就是最淺層的拷貝 次一級的內存塊換個指針馬甲接著用並且務必確定而後被移動對象不被使用可以析構或者重新賦值bbbbla
解決這類問題比較通用的辦法是把這段代碼編譯後反彙編,直接看彙編代碼就知道計算機幹了些什麼。不要擔心彙編看不懂,常用的語法就那麼幾條,看起來肯定比cpp簡單
樓上說c++ move沒有用的,不如直接看代碼, 看輸出:
#include &
#include &
#define log_trace() std::cout &<&< __FILE__ &<&< " " &<&< __PRETTY_FUNCTION__ &<&< " " &<&< __LINE__ &<&< std::endl;
#define log_debug(x) std::cout &<&< __FILE__ &<&< " " &<&< __PRETTY_FUNCTION__ &<&< " " &<&< __LINE__ &<&< " " &<&< x &<&< std::endl;
class Footprint{
public:
Footprint():m_size(0), m_dataPtr(nullptr){
log_trace();
}
explicit Footprint(const std::string data):m_size(data.size()), m_dataPtr(new char[m_size + 1]){
log_trace();
strncpy(m_dataPtr, data.c_str(), m_size);
*(m_dataPtr+m_size) = " ";
}
~Footprint(){
log_trace();
if(m_dataPtr != nullptr){
delete[] m_dataPtr;
m_dataPtr = nullptr;
}
}
Footprint(const Footprint that):m_size(that.m_size), m_dataPtr(new char[m_size + 1]){
log_trace();
strncpy(m_dataPtr, that.m_dataPtr, m_size);
}
Footprint(Footprint that):m_size(that.m_size), m_dataPtr(that.m_dataPtr){
log_trace();
that.m_dataPtr = nullptr;
}
Footprint operator=(const Footprint that){
log_trace();
if(this == that){
return *this;
}
if(m_size == that.m_size){
strncpy(m_dataPtr, that.m_dataPtr, m_size);
*(m_dataPtr+m_size) = " ";
return *this;
}
m_size = that.m_size;
if(m_dataPtr != nullptr){
delete[] m_dataPtr;
m_dataPtr = new char[m_size + 1];
}
strncpy(m_dataPtr, that.m_dataPtr, m_size);
*(m_dataPtr+m_size) = " ";
return *this;
}
Footprint operator=(Footprint that){
log_trace();
if(this == that){
return *this;
}
int32_t sz = m_size;
m_size = that.m_size;
that.m_size = sz;
char* bf = m_dataPtr;
m_dataPtr = that.m_dataPtr;
that.m_dataPtr = bf;
return *this;
}
void print() const{
if(m_dataPtr != nullptr){
log_debug(m_dataPtr);
}
}
private:
int32_t m_size;
char* m_dataPtr;
};
int main(){
Footprint messiah("HalLeLujAh");
messiah.print();
{
Footprint leftRef = messiah;
leftRef.print();
}
{
Footprint rightRef = std::move(messiah);
rightRef.print();
}
return 0;
}
輸出:
move_study.cpp Footprint::Footprint(const string) 14
move_study.cpp void Footprint::print() const 80 HalLeLujAh
move_study.cpp Footprint::Footprint(const Footprint) 27
move_study.cpp void Footprint::print() const 80 HalLeLujAh
move_study.cpp Footprint::~Footprint() 20
move_study.cpp Footprint::Footprint(Footprint) 32
move_study.cpp void Footprint::print() const 80 HalLeLujAh
move_study.cpp Footprint::~Footprint() 20
move_study.cpp Footprint::~Footprint() 20
再看看移動拷貝和普通拷貝函數的實現,節省的時間開銷不是一目了然嗎?
就像輪子哥說的,C++中的右值是一種輔助實現「移動」語義的類型。而「移動」的行為是你自己定義的。
而memmove則是內存上的移動,在現代機器上效率的確高於手動拷貝。std::move,編譯後產生的代碼差不多就是將入參直接返回
推薦閱讀:
※c++ 內聯成員函數問題?
※如何理解c++primer中關於auto的說明?
※新手如何閱讀《C++ Primer》?
※string頭文件和string.h頭文件是一樣的?
※為什麼c++要「在頭文件中聲明,在源文件中定義」?