前不久facebook在github上發布了一個c++工具庫folly,其中的實現大量的使用了c++ 11的新特性,同時,gcc 從4.3版本開始支持c++ 11, 到現在的版本4.8,已經支持了絕大部分c++ 11的新特性(support list),讓我感到時候有必要認真學習一下c++ 11了.關於11引進的新特性概述,已經有很多的文章了,如果你不了解,可以到這里.
今天主要來學習11版本中頗為重要的一個特性,move語義與右值引用,Stackoverflow 上有一篇相當不錯的解釋(原文),我覺得十分有必要翻譯一下,一方面自我學習,一方面分享給大家。由於原文較長,這里分為基礎和加深兩部分來翻譯,對應作者的2個回答。以下是基礎部分:
-----------------------------------------------------------------譯文
我發現理解move語義最簡單的方式是看一個樣例,讓我們從持有一塊動態分配的內存的簡單string類型開始:
1 #include <cstring> 2 #include <algorithm> 3 4 class string 5 { 6 char* data; 7 8 public: 9 10 string(const char* p) 11 { 12 size_t size = strlen(p) + 1; 13 data = new char[size]; 14 memcpy(data, p, size); 15 }
既然我們要自己管理內存,那我們就應該遵守那三條原則(the rule of three),如果你不知道什么三條原則,去c++ 98標准里查找。下面我將推遲賦值運算符的實現,先來實現復制構造函數和析構函數:
1 ~string() 2 { 3 delete[] data; 4 } 5 6 string(const string& that) 7 { 8 size_t size = strlen(that.data) + 1; 9 data = new char[size]; 10 memcpy(data, that.data, size); 11 }
復制構造函數定義了怎樣復制一個string對象。參數const string& that 可以為想要復制string的以下例子中的任何一種形式:
1 string a(x); // line 1 2 string b(x + y); // line 2 3 string c(some_function_returning_a_string()); // line 3
現在我們開始分析move語義。你會發現,只有第一行(line 1)的x深度拷貝是有必要的,因為我們可能會在后邊用到x,如果x改變了,我們會很奇怪。你有沒有注意到我剛剛把x說了三遍(如果包括這次的話,是四遍),每一遍都是說的同一個對象?我們把x的這種表達式叫做左值(lvalues)。
第二行和第三行的參數就不是左值而是右值,因為表達式產生的string對象是匿名對象,之后沒有辦法再使用了。右值就是指在下一個分號(更准確的說是在包含右值的完整表達式的最后)銷毀的臨時對象。這一點非常重要,因為我們可以在b和c的初始化過程中,對源string對象(參數)做任何想要做的事情,並不讓用戶感覺到。
C++ 11引入了一種新的機制叫做“右值引用”,以便我們通過重載直接使用右值參數。我們所要做的就是寫一個以右值引用為參數的構造函數。在這個函數的內部,我們對參數所指向的對象做任何事情,只要我們保持他的合理性:
1 string(string&& that) // string&& is an rvalue reference to a string 2 { 3 data = that.data; 4 that.data = 0; 5 }
我們在這里是怎么做的呢?我們沒有深度拷貝堆內存中的數據,而是僅僅復制了指針,並把源對象的指針置空。事實上,我們“偷取”了屬於源對象的內存數據。再次,問題的關鍵變成:無論在任何情況下,都不能讓客戶覺察到源對象被改變了。在這里,我們並沒有真正的復制,所以我們把這個構造函數叫做“轉移構造函數”(move constructor, 不知道譯法是否確切),他的工作就是把資源從一個對象轉移到另一個對象,而不是復制他們。
祝賀你,你現在對move語義有了基礎的理解,我們繼續來進行賦值操作符的實現。如果你不理解copy and swap慣用法,先去學習 一下,然后再回來,因為這是c++異常安全的一個非常精彩的慣用法。
1 string& operator=(string that) 2 { 3 std::swap(data, that.data); 4 return *this; 5 } 6 };
呃,就這些?右值引用在哪里?你可能會這樣問。我的答案是:在這里,我們不需要
注意到我們是直接對參數that傳值,所以that會像其他任何對象一樣被初始化,那么確切的說,that是怎樣被初始化的呢?對於C++ 98,答案是復制構造函數,但是對於C++ 11,編譯器會依據參數是左值還是右值在復制構造函數和轉移構造函數間進行選擇。
如果是a=b,這樣就會調用復制構造函數來初始化that(因為b是左值),賦值操作符會與新創建的對象交換數據,深度拷貝。這就是copy and swap 慣用法的定義:構造一個副本,與副本交換數據,並讓副本在作用域內自動銷毀。這里也一樣。
如果是a = x + y,這樣就會調用轉移構造函數來初始化that(因為x+y是右值),所以這里沒有深度拷貝,只有高效的數據轉移。相對於參數,that依然是一個獨立的對象,但是他的構造函數是無用的(trivial),因此堆中的數據沒有必要復制,而僅僅是轉移。沒有必要復制他,因為x+y是右值,再次,從右值指向的對象中轉移是沒有問題的。
總結一下:復制構造函數執行的是深度拷貝,因為源對象本身必須不能被改變。而轉移構造函數卻可以復制指針,把源對象的指針置空,這種形式下,這是安全的,因為用戶不可能再使用這個對象了。
我希望這個例子抓住了要點,為了簡單起見,關於右值引用和move語義的很多內容我都故意略過了。