如何評價 C++11 的右值引用(Rvalue reference)特性?

雖然它解決了一些很實際的問題,但我個人總覺得這個特性有些怪怪的。將這樣一個問題留給程序員解決,有一種讓本來就不算漂亮的C++語法更加臃腫的感覺。它是不是一種缺乏GC機制的語言為了性能的必要折衷?請問各位怎樣看待它?


右值引用是C++11中最重要的新特性之一,它解決了C++中大量的歷史遺留問題,使C++標準庫的實現在多種場景下消除了不必要的額外開銷(如std::vector, std::string),也使得另外一些標準庫(如std::unique_ptr, std::function)成為可能。即使你並不直接使用右值引用,也可以通過標準庫,間接從這一新特性中受益。為了更好的理解標準庫結合右值引用帶來的優化,我們有必要了解一下右值引用的重大意義。

右值引用的意義通常解釋為兩大作用:移動語義和完美轉發。本文主要討論移動語義。

移動語義

======

移動語義,簡單來說解決的是各種情形下對象的資源所有權轉移的問題。而在C++11之前,移動語義的缺失是C++飽受詬病的問題之一。

舉個栗子。

問題一:如何將大象放入冰箱?

答案是眾所周知的。首先你需要有一台特殊的冰箱,這台冰箱是為了裝下大象而製造的。你打開冰箱門,將大象放入冰箱,然後關上冰箱門。

問題二:如何將大象從一台冰箱轉移到另一台冰箱?

普通解答:打開冰箱門,取出大象,關上冰箱門,打開另一台冰箱門,放進大象,關上冰箱門。

2B解答:在第二個冰箱中啟動量子複製系統,克隆一隻完全相同的大象,然後啟動高能激光將第一個冰箱內的大象氣化消失。

等等,這個2B解答聽起來很耳熟,這不就是C++中要移動一個對象時所做的事情嗎?

「移動」,這是一個三歲小孩都明白的概念。將大象(資源)從一台冰箱(對象)移動到另一台冰箱,這個行為是如此自然,沒有任何人會採用先複製大象,再銷毀大象這樣匪夷所思的方法。C++通過拷貝構造函數和拷貝賦值操作符為類設計了拷貝/複製的概念,但為了實現對資源的移動操作,調用者必須使用先複製、再析構的方式。否則,就需要自己實現移動資源的介面。

為了實現移動語義,首先需要解決的問題是,如何標識對象的資源是可以被移動的呢?這種機制必須以一種最低開銷的方式實現,並且對所有的類都有效。C++的設計者們注意到,大多數情況下,右值所包含的對象都是可以安全的被移動的。

右值(相對應的還有左值)是從C語言設計時就有的概念,但因為其如此基礎,也是一個最常被忽略的概念。不嚴格的來說,左值對應變數的存儲位置,而右值對應變數的值本身。C++中右值可以被賦值給左值或者綁定到引用。類的右值是一個臨時對象,如果沒有被綁定到引用,在表達式結束時就會被廢棄。於是我們可以在右值被廢棄之前,移走它的資源進行廢物利用,從而避免無意義的複製。被移走資源的右值在廢棄時已經成為空殼,析構的開銷也會降低。

右值中的數據可以被安全移走這一特性使得右值被用來表達移動語義。以同類型的右值構造對象時,需要以引用形式傳入參數。右值引用顧名思義專門用來引用右值,左值引用和右值引用可以被分別重載,這樣確保左值和右值分別調用到拷貝和移動的兩種語義實現。對於左值,如果我們明確放棄對其資源的所有權,則可以通過std::move()來將其轉為右值引用。std::move()實際上是static_cast&()的簡單封裝。

右值引用至少可以解決以下場景中的移動語義缺失問題:

  • 按值傳入參數

按值傳參是最符合人類思維的方式。基本的思路是,如果傳入參數是為了將資源交給函數接受者,就應該按值傳參。同時,按值傳參可以兼容任何的cv-qualified左值、右值,是兼容性最好的方式。

class People {
public:
People(string name) // 按值傳入字元串,可接收左值、右值。接收左值時為複製,接收右值時為移動
: name_(move(name)) // 顯式移動構造,將傳入的字元串移入成員變數
{
}
string name_;
};

People a("Alice"); // 移動構造name

string bn = "Bob";
People b(bn); // 拷貝構造name

構造a時,調用了一次字元串的構造函數和一次字元串的移動構造函數。如果使用const string name接收參數,那麼會有一次構造函數和一次拷貝構造,以及一次non-trivial的析構。儘管看起來很蛋疼,儘管編譯器還有優化,但從語義來說按值傳入參數是最優的方式。

如果你要在構造函數中接收std::shared_ptr&並且存入類的成員(這是非常常見的),那麼按值傳入更是不二選擇。拷貝std::shared_ptr&需要線程同步,相比之下移動std::shared_ptr是非常輕鬆愉快的。

  • 按值返回

和接收輸入參數一樣,返回值按值返回也是最符合人類思維的方式。曾經有無數函數為了返回容器而不得不寫成這樣

void str_split(const string s, vector&* vec); // 一個按值語義定義的字元串拆分函數。這裡不考慮分隔符,假定分隔符是固定的。

這樣要求vec在外部被事先構造,此時尚無從得知vec的大小。即使函數內部有辦法預測vec的大小,因為函數並不負責構造vec,很可能仍需要resize。

對這樣的函數嵌套調用更是痛苦的事情,誰用誰知道啊。

有了移動語義,就可以寫成這樣

vector& str_split(const string s) {
vector& v;
// ...
return v; // v是左值,但優先移動,不支持移動時仍可複製。
}

如果函數按值返回,return語句又直接返回了一個棧上的左值對象(輸入參數除外)時,標準要求優先調用移動構造函數,如果不符再調用拷貝構造函數。儘管v是左值,仍然會優先採用移動語義,返回vector&從此變得雲淡風輕。此外,無論移動或是拷貝,可能的情況下仍然適用編譯器優化,但語義不受影響。

對於std::unique_ptr來說,這簡直就是福音。

unique_ptr& create_obj(/*...*/) {
unique_ptr& ptr(new SomeObj(/*...*/));
ptr-&>foo(); // 一些可能的初始化
return ptr;
}

當然還有更簡單的形式

unique_ptr& create_obj(/*...*/) {
return unique_ptr&(new SomeObj(/*...*/));
}

在工廠類中,這樣的語義是非常常見的。返回unique_ptr能夠明確對所構造對象的所有權轉移,特別的,這樣的工廠類返回值可以被忽略而不會造成內存泄露。上面兩種形式分別返回棧上的左值和右值,但都適用移動語義(unique_ptr不支持拷貝)。

  • 接收右值表達式

沒有移動語義時,以表達式的值(例為函數調用)初始化對象或者給對象賦值是這樣的:

vector& str_split(const string s);

vector& v = str_split("1,2,3"); // 返回的vector用以拷貝構造對象v。為v申請堆內存,複製數據,然後析構臨時對象(釋放堆內存)。
vector& v2;
v2 = str_split("1,2,3"); // 返回的vector被複制給對象v(拷貝賦值操作符)。需要先清理v2中原有數據,將臨時對象中的數據複製給v2,然後析構臨時對象。

註:v的拷貝構造調用有可能被優化掉,儘管如此在語義上仍然是有一次拷貝操作。

同樣的代碼,在支持移動語義的世界裡就變得更美好了。

vector& str_split(const string s);

vector& v = str_split("1,2,3"); // 返回的vector用以移動構造對象v。v直接取走臨時對象的堆上內存,無需新申請。之後臨時對象成為空殼,不再擁有任何資源,析構時也無需釋放堆內存。
vector& v2;
v2 = str_split("1,2,3"); // 返回的vector被移動給對象v(移動賦值操作符)。先釋放v2原有數據,然後直接從返回值中取走數據,然後返回值被析構。

註:v的移動構造調用有可能被優化掉,儘管如此在語義上仍然是有一次移動操作。

不用多說也知道上面的形式是多麼常用和自然。而且這裡完全沒有任何對右值引用的顯式使用,性能提升卻默默的實現了。

  • 對象存入容器

這個問題和前面的構造函數傳參是類似的。不同的是這裡是按兩種引用分別傳參。參見std::vector的push_back函數。

void push_back( const T value ); // (1)
void push_back( T value ); // (2)

不用多說自然是左值調用1右值調用2。如果你要往容器內放入超大對象,那麼版本2自然是不2選擇。

vector&&> vv;

vector& v = {"123", "456"};
v.push_back("789"); // 臨時構造的string類型右值被移動進容器v
vv.push_back(move(v)); // 顯式將v移動進vv

困擾多年的難言之隱是不是一洗了之了?

  • std::vector的增長

又一個隱蔽的優化。當vector的存儲容量需要增長時,通常會重新申請一塊內存,並把原來的內容一個個複製過去並刪除。對,複製並刪除,改用移動就夠了。

對於像vector&這樣的容器,如果頻繁插入造成存儲容量不可避免的增長時,移動語義可以帶來悄無聲息而且美好的優化。

  • std::unique_ptr放入容器

曾經,由於vector增長時會複製對象,像std::unique_ptr這樣不可複製的對象是無法放入容器的。但實際上vector並不複製對象,而只是「移動」對象。所以隨著移動語義的引入,std::unique_ptr放入std::vector成為理所當然的事情。

容器中存儲std::unique_ptr有太多好處。想必每個人都寫過這樣的代碼:

MyObj::MyObj() {
for (...) {
vec.push_back(new T());
}
// ...
}

MyObj::~MyObj() {
for (vector&::iterator iter = vec.begin(); iter != vec.end(); ++iter) {
if (*iter) delete *iter;
}
// ...
}

繁瑣暫且不說,異常安全也是大問題。使用vector&&>,完全無需顯式析構,unqiue_ptr自會打理一切。完全不用寫析構函數的感覺,你造嗎?

unique_ptr是非常輕量的封裝,存儲空間等價於裸指針,但安全性強了一個世紀。實際中需要共享所有權的對象(指針)是比較少的,但需要轉移所有權是非常常見的情況。auto_ptr的失敗就在於其轉移所有權的繁瑣操作。unique_ptr配合移動語義即可輕鬆解決所有權傳遞的問題。

註:如果真的需要共享所有權,那麼基於引用計數的shared_ptr是一個好的選擇。shared_ptr同樣可以移動。由於不需要線程同步,移動shared_ptr比複製更輕量。

  • std::thread的傳遞

thread也是一種典型的不可複製的資源,但可以通過移動來傳遞所有權。同樣std::future std::promise std::packaged_task等等這一票多線程類都是不可複製的,也都可以用移動的方式傳遞。

完美轉發

======

除了移動語義,右值引用還解決了C++03中引用語法無法轉發右值的問題,實現了完美轉發,才使得std::function能有一個優雅的實現。這部分不再展開了。

總結

======

移動語義絕不是語法糖,而是帶來了C++的深刻革新。移動語義不僅僅是針對庫作者的,任何一個程序員都有必要去了解它。儘管你可能不會去主動為自己的類實現移動語義,但卻時時刻刻都在享受移動語義帶來的受益。因此這絕不意味著這是一個可有可無的東西。


我的理解,右值引用意在解決棧區對象的所有權轉移問題

對於在堆區分配的對象而言,轉移對象所有權很簡單,大致就是指針賦值。

在C++11以前,棧區的對象只能被deep copy,而不能轉移所有權,這就導致了一些確實要轉移所有權而 deep copy 的不必要開銷,比如函數返回臨時對象給外界時會多次調用構造函數。

右值引用允許一個類定義轉移構造函數,比如有一個Vector類:

class Vector {
public:
Vector(int len, int initial_value) : len_(len) {
elements_ = new int[len];
memset(elements_, initial_value, len * sizeof(int));
}

Vector(const Vector v) : len_(v.len_) {
elements_ = new int[len];
memcpy(elements_, v.elements_, len * sizeof(int));
}

...

private:
int *elements_;
int len_;
};

這時如果我們要寫一個函數,返回一個在棧區分配空間的Vector對象:

Vector ConstructVector(int len) {
return Vector(len, 0);
}

Vector v = ConstructVector(1000000);

這個函數調用的開銷不小,不開編譯器優化時會大概調用一次構造函數,2次deep copy。

優化點在於,函數中return後面的對象是個Vector臨時對象,何必對它做deep copy呢?

於是在C++ 11中,我們為 Vector類定義轉移構造函數:

Vector(Vector v) : len_(len) {
elements_ = v.elements_;
}

這樣當我們調用ConstructVector時就不會觸發 deep copy了。

Vector 表示對右值的引用。轉移構造函數後,通過Vector v傳入的對象狀態是未定義的。


在其他語言在面向堆空間、面向GC、面向引用編程的時候,C++在面向棧空間、面向生命周期管理、面向值類型的native編程路線上越走越遠也越走越深,成為了一門時髦的編譯語言。

右值引用就是值類型編程中重要的一部分,沒有它你就不能以很小的代價返回一個對象,以前我們都必須返回一個指針然後要求調用方delete,現在就不一樣了,不管是直接返回對象還是返回智能指針都很安全。


我不同意LS的觀點:「但是沒有根本上的存在必要」。C++11 右值引用的定位有點像C++的模板,右值引用極大的提高了C++效率,主要應用於一些庫(STL)中,實際項目開發用的少,但是並不是沒有存在的必要。http://cpp1x.org/R-value-Ref-And-Move-Construct.html


一句話答案:右值引用的出現是為了實現移動語義,順便解決完美轉發的問題,其意義在於擴充了值語義,幫助Modern C++可以全面地應用RAII。

以下展開:

------------------------

  • 澄清誤區

在具體回答右值引用的價值之前,我想先否定題主的兩個提法。

首先是

"將這樣一個問題留給程序員解決"

右值引用並沒有將什麼「問題」留給程序員解決,而是向程序員提供更多的選擇。正如Bjarne Stroustrup所說,

"C++的許多設計決策根源於我對強迫人按某種特定方式行事的極度厭惡。"

"當我試圖去取締一個我不喜歡的語言特徵時,我總抑制住這樣的慾望,因為我認為我無權把個人觀點強加給別人。"

選擇是自主權的表現,嚮往自由的人一定不會認為有選擇是件壞事。

更何況右值引用的神奇之處在於,在很多時候,使用函數庫的程序員根本不用關心它的存在。例如:

auto vec = init_data(args);
auto pi = make_shared&(11);
auto ei = resolver.resolve({"localhost", "1024"});

另一個是

"一種缺乏GC機制的語言為了性能的必要折衷"

由於有RAII的存在,C++通常是不需要GC的,因為RAII能讓C++代碼同時具備簡潔高性能(在某些特定的高並發環境里GC性能可能更高)和異常安全這三個特點。所以,C++暫時還不需要任何特性來作這種所謂的折衷。

  • 為什麼會覺得右值引用很怪?

根本原因是不理解值語義(Value Semantics)

值語義是很多OO語言里沒有的概念。在這些語言里,幾乎所有的變數都是引用語義(Reference Semantics),GC掌管了所有對象的生命期管理事務, 程序員無需為此操心,只需要用變數去對象池中去引用即可。值語義雖然也有出場的機會,例如Java里的Primitive Data Type,但畢竟不是重點,所以往往被忽視。

還有一個很重要的原因,在OO語言里,

值語義和運行時多態是矛盾的。

值語義因此也成為了C++區別於其它OO語言的特徵之一。

所以,在不理解值語義的情況下去談右值引用,最多也是淺嘗輒止,所以"感覺奇怪"也是很正常的,甚至會出現類似「C++的數組不支持多態」?這樣的問題。

  • 值語義的作用

首先給出一篇blog作為值語義的科普,內容則不再複述,以下只討論其意義。

這篇blog的作者Andrzej Krzemieński,曾在另一篇blog中盛讚RAII,並認為它是C++"s best feature,Herb Sutter也在文章的回復中提到:

RAII code is just naturally exception-safe with immediate cleanup, not leaking stuff until the GC gets around to finding it. This is especially important with scarce resources like file handles.

但問題是,像C#的using statement和Java的try-with-resources statement同樣具有RAII的特點,但仍然有人會提出"RAII: Why is unique to C++?"這樣的問題。原因即在於C++獨有的值語義:程序員通過值語義可以方便直觀地控制對象生命期,讓RAII用起來更自然。

更何況像這段代碼,

vector& produce()
{
vector& ans;
for (int i = 0; i &< 10; ++i) { ans.emplace_back(name(i)); } return ans; } void consumer() { vector& files = produce();
for (ifstream f: files) {
read(f);
}
}

用try-with-resources根本就搞不定。當然,finally還是可以搞定,只是代碼會很醜。

所以,值語義在C++里的作用之一就是用於控制對象生命期(另一個作用就是提升性能),以方便通過RAII寫出簡潔自然、異常安全的代碼。該意義非常重大,這也是右值引用在C++ - State of the Evolution上穩坐頭把交椅的原因。

  • 右值引用與值語義

前面提到,值語義用於控制對象的生命期,而其具體的控制方式分為兩種:

    • 生命期限於scope內:無需控制,到期自動調用析構函數。
    • 需要延長到scope外:移動語義。

因為右值引用的目的在於實現移動語義,所以右值引用 意義即是加強了值語義對對象生命期的控制能力

在移動語義的作用下,Effective C++ 條款23從此作古:

當你必須傳回object時,不要嘗試傳回reference。

資源管理類,如std::ifstream、boost::asio::basic_stream_socket,可以光明正大地當成int來用,而無需擔心類似Effective STL 條款8那樣的問題:

切勿創建包含auto_ptr的容器對象。

  • 右值引用與完美轉發

右值引用還有一個作用是實現完美轉發。完美轉發可以在一定程度上讓代碼保持簡潔,但同時,這也引入了一些令人討厭的坑。個人感覺意義不如移動語義的重大,所以這裡不再展開了。

---------------

附參考資料

  • C++ future and the pointer. Jens Weller

  • Back to the Basics! Essentials of Modern C++ Style. Herb Sutter

  • Move Constructor. Andrzej Krzemieński

  • A proposal to Add Move Semantics Support to C++ Language. Howard E. Hinnant, Peter Dimov, Dave Abrahams

  • C++ 工程實踐(8):值語義. 陳碩


初學C++, 瞎寫了一堆腦補的錯誤結論.

感謝 金曉 的耐心評論. 重名太多, 我@不到了.

不刪答案以表尊重.


C++ 的演化過程有點打補丁的感覺。先用一塊補丁修補了C的不足,後來遇到問題,再打個補丁來修改。因為需要兼容以前的代碼,不能直接拆掉舊補丁,而只能用新補丁直接覆蓋在舊補丁上面。

C++ 的功能特性,都有現實中的需求。當你沒有遇到需求的時候,會覺得那個特性是多餘的。但當遇到的時候,就會覺得有這種特性是多麼美妙的事情。右值引用就是這樣的特性。從前沒有右值引用的時候,為避免臨時對象的多餘複製,和實現函數的完美轉發,使了很多古怪手段,當有了右值引用,解決相同的問題,已經簡單很多了。

問題本身是複雜的,當不能用簡單的手段來解決時,也只好用稍微複雜點的手段來解決了。這總比採用鴕鳥政策,當問題不存在要好。理解了右值引用需要解決什麼問題,自然可以很好理解右值引用了。


看別人寫一個非平凡的數據結構,如果不實現右值構造和operator=,我會感覺很難受。


內存模型外,C++11 最重要的特性,想理解這一點,必須結合完美轉發及變參模板。


  • const 既可以綁定左值,又可以綁定右值,

  • 只可以綁定左值。

不覺得少了點什麼嗎?


右值引用的本質是提供了一種系統的方法,可以在編譯時區分臨時對象(常常是匿名的)和具名對象,使得某些對構造對象敏感的代碼可以為此優化,省略可能繁重的構造析構以及拷貝操作。這是典型的零開銷原則的實例之一。你不使用沒什麼影響,但是使用了卻可以在某些情況下極大地提升性能。這樣的特性當然是多多益善啦。

另,這樣的特性主要是針對庫作者的。


對於這個問題,我想講一個故事,舉一個例子。如果有什麼問題,希望大家不吝指出

===========================================================

很久之前,有一個小鎮,鎮上有兩幢房子,房子1和房子2。房子1的主人是A,房子2的主人是B。其他人都沒有房子住。鎮上有個規律,房子必須一直有人看著,如果沒人看著,就會被別人霸佔了。原來一直都相安無事。

有一天房子1的主人A和房子2的主人B不知怎麼的,同時喜歡上了對方的房子,怎麼辦呢,一離開自己的房子就沒有了,怎麼交換房子呢,有倆辦法。

第一個辦法:先找來一個人C,照著A房子的建一幢3號房子,和1號房子一模一樣,然後守住。然後A會拆了1號房子,按照2號房子建。建完之後,B拆了2號房子,按照3號房子,即原來的1號房子建一幢房子,建好後就可以了

第二個辦法:找來一個人C,他佔住1號房子,然後A跑到2號房子,等A跑到2號房子,B再跑到1號房子去,C自行離開,即可

===========================================================

對於上面的故事,是不是很眼熟,其實就是交換函數了

對於交換變數的代碼,我們原來的寫法如下:

void swap(int a,int b){
int tmp = a;
a = b;
b = tmp;
}

需要三次複製

現在的寫法改成如下所示:

void swap(int a ,int b){
int tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}

一次複製也不需要

對比一下是不是有相似的感覺

其實右值引用的實質我感覺其實是所有權的轉讓,原來的數據沒有被釋放,被轉讓給個左值變數進行使用。因此省略了複製或重新構建的時間,效率會帶來明顯的提升


As a C++ programmer, you don"t pay for what you don"t need.

如果你不需要rvalue ref,你可以不用,即使用了其他人的庫當中有rvlue ref,對你來說基本也是透明的。

沒有右值語意,unique_ptr還得像原來的auto_ptr一樣各種潛在錯誤。

有了右值,你的swap生成的機器碼可以僅僅使用三條機器指令就交換兩個對象的內容。

至於GC,有一句話是這麼說的:如果有GC,那c++還剩什麼。


右值,不是一個新東西。早在60年代,FORTRAN、ALGOL、 LISP流行時,人們就認識到了值應該分類為左值與右值。左值是可以存儲值的地址;右值是可以被複制存儲的值內容。在C語言孕育、發明、初步推廣時,左值與右值就是常識性的認識了。

但是,那時候看起來值分類(value categories)為左值、右值,每個卵用,完全是研究者的玄學之談。所以,你學C語言時從來沒讀過、想過左值右值,也很正常。

C++11正式推出的右值引用(Rvalue reference)、瀕危值(xvalue)才是真正的新東西,是現在的計算機本科生都該學習的,讓男程序員沉默、女程序員流淚

什麼是右值引用(Rvalue reference)? 你寫一個很簡單的幾行程序,來個類型顯式轉換為右值引用或者調用一個完美轉發(perfect forward),然後看看編譯後對應的彙編語句,就知道在彙編這一層面,右值引用就是當作個指針,完美轉發其實啥都沒做。

既然右值引用實現時就是個指針,那麼它與傳統的(左值)引用有什麼區別? 答案是,在彙編層面上,二者沒有區別。 (題外話, 如果你的程序動態載入一個DLL,然後載入DLL的一個輸出函數,這個函數的某個參數在實現時是傳統的(左值)引用,那麼你現在只能把它看作指針型參數來使用)。

右值引用在編譯器之下的層面就是指針這麼簡單的東西,那在編譯器之上的C++源代碼中,為什麼引入右值引用這個新概念? 我認為,其目的、意義就是讓編程者與編譯器,能區分兩種情形:

a. 使用傳統左值引用的複製語義

b. 使用右值引用的的移動語義。

你把一個對象「移動」方式存入容器對象,別人並不知道這個對象到底該如何「移動」,必須是你來寫一個移動構造函數,具體把這個類型的對象的「移動」給實作出來。

container.push_back(argument_expression); 這麼個語句,編譯器就可以根據實參表達式的值分類,來確定應該重載(overload)調用複製語義版本的push_back(Type param),還是移動語義版本的push_back(Type param)


可以看看:

《C++0x漫談》系列之:右值引用(或「move語意與完美轉發」)(上)

《C++0x漫談》系列之:右值引用(或「move語意與完美轉發」)(下)

摘自上面的話:

右值引用(及其支持的Move語意和完美轉發)是C++0x將要加入的最重大語言特性之一,這點從該特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出來。從實踐角度講,它能夠完美解決C++中長久以來為人所詬病的臨時對象效率問題。從語言本身講,它健全了C++中的引用類型在左值右值方面的缺陷。從庫設計者的角度講,它給庫設計者又帶來了一把利器。從庫使用者的角度講,不動一兵一卒便可以獲得「免費的」效率提升…

像1樓的那個搬運大象比喻,很好的描述了move和copy

std::vector readFile()
{
std::vector retv;
… // fill retv
return retv;
}

這段代碼低效的地方在於那個返回的臨時對象。一整個vector得被拷貝一遍,僅僅是為了傳遞其中的一組int,當v被構造完畢之後,這個臨時對象便煙消雲散。

這完全是公然的浪費!

出現這種情況,編譯器一般都會乖乖放棄優化。

但對編譯器來說這還不是最鬱悶的一種情況,最鬱悶的是:

std::vector& v;
v = readFile(); // assignment, not copy construction

正常是這樣寫的

void readFile(vector v){ … // fill v }

這當然可以。

但是如果遇到操作符重載呢?

string operator+(string const s1, string const s2);

而且,就算是對於readFile,原先的返回vector的版本支持

BOOST_FOREACH(int i, readFile()){
… // do sth. with i
}

改成引用傳參後,原本優雅的形式被破壞了,為了進行以上操作不得不引入一個新的名字,這個名字的存在只是為了應付被破壞的形式,一旦foreach操作結束它短暫的生命也隨之結束:

vector& v;
readFile(v);
BOOST_FOREACH(int I, v){
}
// v becomes useless here

還有什麼問題嗎?自己去發現吧。總之,利用引用傳參是一個解決方案,但其能力有限.

Move語意

最初的例子——完美解決方案

在先前的那個例子中

vector& v = readFile();

有了move語意的話,readFile就可以簡單的改成:

std::vector& readFile()
{
std::vector& retv;
… // fill retv
return std::move(retv); // move retv out
}


說的我個人的看法,僅供參考.雖然右值引用提供了一種機制,可以區分右值引用的,但是即使沒有這種機制,一樣也有解決辦法.

對拷貝構造,現在的編譯器已經很聰明了,前面說的那些放棄N)RVO優化的那些情況,那是90年代的某個人在新聞組裡說的吧.

那麼賦值呢,在沒有右值引用的時候,自己寫個move可以了嘛,而且你可以寫各種不同的move精細控制.


C++很善於解決C++製造的問題

然而問題並沒有完全解決,一個struct裡面如果有一百個int,我就不得不去找其他「變通」的方法。

祝c艹粉在沒有gc的世界活得幸福!


對語意從語法上加以支持,一是有利於代碼的表達,二是為優化留下空間。

可以參考引用,const的作用。


說白了,就是約定一種機制讓大家在不該拷貝的地方不要拷貝了,這樣程序會快一點

至於藉此實現的完美轉發特性,感覺對大多數碼業務的程序員來說並無太大受益


就C++這個畸形種而言,R-ref以及std::move/std::swap成功地在保持代碼兼容的情況下大大提高了實際程序的性能(至少就std自帶庫而言,因為非std自帶庫你要自己確保帶的overload),可以說是一個很不錯的妥協的發明了。

至少就我而言我是想不到這樣的創意。雖然說我對於R-ref所針對的C++設計缺陷有很多想法,然而並不能做到保持代碼兼容/不違背C++一些既存原則


右值引用和GC沒有關係,有GC的語言也有這個問題,只是它們沒有打算解決。比如java中

字元串拼接 String d = a + b + c;

不過C++的右值引用真的不是最好的解決方案,最大的原因是它要兼容已經有代碼。

如果能把左值改為右值,而不影響所有已經有代碼,也許更理想一些。

當年BIG 4已經變成BIG 3/4/5了,這個真的是一個難題。


推薦閱讀:

零基礎(轉行)能學unity3d嗎?
什麼是"Core Dumps",為什麼"Haskell"可以沒有?
虎書ML版裡面關於garbage collector的問題?
如何評價 RAII 特性在 C++ 中的大範圍運用?
明明很多編程語言從對象數組中取數據比用SQL從表中取數據方便,為什麼資料庫還是使用SQL?

TAG:編程語言 | 計算機科學 | C11 |