1、右值引用引入的背景
臨時對象的產生和拷貝所帶來的效率折損,一直是C++所為人诟病的問題。但是C++標准允許編譯器對於臨時對象的產生具有完全的自由度,從而發展出了Copy Elision、RVO(包括NRVO)等編譯器優化技術,它們可以防止某些情況下臨時對象產生和拷貝。下面簡單地介紹一下Copy Elision、RVO,對此不感興趣的可以直接跳過:
(1) Copy Elision
Copy Elision技術是為了防止某些不必要的臨時對象產生和拷貝,例如:
struct A { A(int) {} A(const A &) {} }; A a = 42;
理論上講,上述A a = 42;語句將分三步操作:第一步由42構造一個A類型的臨時對象,第二步以臨時對象為參數拷貝構造a,第三步析構臨時對象。如果A是一個很大的類,那麼它的臨時對象的構造和析構將造成很大的內存開銷。我們只需要一個對象a,為什麼不直接以42為參數直接構造a呢?Copy Elision技術正是做了這一優化。
【說明】:你可以在A的拷貝構造函數中加一打印語句,看有沒有調用,如果沒有被調用,那麼恭喜你,你的編譯器支持Copy Elision。但是需要說明的是:A的拷貝構造函數雖然沒有被調用,但是它的實現不能沒有訪問權限,不信你將它放在private權限裡試試,編譯器肯定會報錯。
(2) 返回值優化(RVO,Return Value Optimization)
返回值優化技術也是為了防止某些不必要的臨時對象產生和拷貝,例如:
struct A { A(int) {} A(const A &) {} }; A get() {return A(1);} A a = get();
理論上講,上述A a = get();語句將分別執行:首先get()函數中創建臨時對象(假設為tmp1),然後以tmp1為參數拷貝構造返回值(假設為tmp2),最後再以tmp2為參數拷貝構造a,其中還伴隨著tmp1和tmp2的析構。如果A是一個很大的類,那麼它的臨時對象的構造和析構將造成很大的內存開銷。返回值優化技術正是用來解決此問題的,它可以避免tmp1和tmp2兩個臨時對象的產生和拷貝。
【說明】: a)你可以在A的拷貝構造函數中加一打印語句,看有沒有調用,如果沒有被調用,那麼恭喜你,你的編譯器支持返回值優化。但是需要說明的是:A的拷貝構造函數雖然沒有被調用,但是它的實現不能沒有訪問權限,不信你將它放在private權限裡試試,編譯器肯定會報錯。
b)除了返回值優化,你可能還聽說過一個叫具名返回值優化(Named Return Value Optimization,NRVO)的優化技術,從程序員的角度而言,它其實跟RVO同樣的邏輯。只是它的臨時對象具有變量名標識,例如修改上述get()函數為:
A get() { A tmp(1); // #1 // do something return tmp; } A a = get(); // #2
想想上述修改後A類型共有幾次對象構造?雖然#1處看起來有一次顯示地構造,#2處看起來也有一次顯示地構造,但如果你的編譯器支持NRVO和Copy Elision,你會發現整個A a = get();語句的執行過程,只有一次A對象的構造。如果你在get()函數return語句前打印tmp變量的地址,在A a = get();語句後打印a的地址,你會發現兩者地址相同,這就是應用了NRVO技術的結果。
(3) Copy Elision、RVO無法避免的臨時對象的產生和拷貝
雖然Copy Elision和NVO(包括NRVO)等技術能避免一些臨時對象的產生和拷貝,但某些情況下它們卻發揮不了作用,例如:
templatevoid swap(T& a, T& b) { T tmp(a); a = b; b = tmp; }
我們只是想交換a和b兩個對象所擁有的數據,但卻不得不使用一個臨時對象tmp備份其中一個對象,如果T類型對象擁有指向(或引用)從堆內存分配的數據,那麼深拷貝所帶來的內存開銷是可以想象的。為此,C++11標准引入了右值引用,使用它可以使臨時對象的拷貝具有move語意,從而可以使臨時對象的拷貝具有淺拷貝般的效率,這樣便可以從一定程度上解決臨時對象的深度拷貝所帶來的效率折損。
2、C++03標准中的左值與右值
要理解右值引用,首先得區分左值(lvalue)和右值(rvalue)。
C++03標准中將表達式分為左值和右值,並且“非左即右”:
Every expression is either an lvalue or an rvalue.
區分一個表達式是左值還是右值,最簡便的方法就是看能不能夠對它取地址:如果能,就是左值;否則,就是右值。
【說明】:由於右值引用的引入,C++11標准中對表達式的分類不再是“非左即右”那麼簡單,不過為了簡單地理解,我們暫時只需區分左值右值即可,C++11標准中的分類後面會有描述。
3、右值引用的綁定規則
右值引用(rvalue reference,&&)跟傳統意義上的引用(reference,&)很相似,為了更好地區分它們倆,傳統意義上的引用又被稱為左值引用(lvalue reference)。下面簡單地總結了左值引用和右值引用的綁定規則(函數類型對象會有所例外):
(1)非const左值引用只能綁定到非const左值;
(2)const左值引用可綁定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能綁定到非const右值;
(4)const右值引用可綁定到const右值和非const右值。
測試例子如下:
struct A { A(){} }; A lvalue; // 非const左值對象 const A const_lvalue; // const左值對象 A rvalue() {return A();} // 返回一個非const右值對象 const A const_rvalue() {return A();} // 返回一個const右值對象 // 規則一:非const左值引用只能綁定到非const左值 A &lvalue_reference1 = lvalue; // ok A &lvalue_reference2 = const_lvalue; // error A &lvalue_reference3 = rvalue(); // error A &lvalue_reference4 = const_rvalue(); // error // 規則二:const左值引用可綁定到const左值、非const左值、const右值、非const右值 const A &const_lvalue_reference1 = lvalue; // ok const A &const_lvalue_reference2 = const_lvalue; // ok const A &const_lvalue_reference3 = rvalue(); // ok const A &const_lvalue_reference4 = const_rvalue(); // ok // 規則三:非const右值引用只能綁定到非const右值 A &&rvalue_reference1 = lvalue; // error A &&rvalue_reference2 = const_lvalue; // error A &&rvalue_reference3 = rvalue(); // ok A &&rvalue_reference4 = const_rvalue(); // error // 規則四:const右值引用可綁定到const右值和非const右值,不能綁定到左值 const A &&const_rvalue_reference1 = lvalue; // error const A &&const_rvalue_reference2 = const_lvalue; // error const A &&const_rvalue_reference3 = rvalue(); // ok const A &&const_rvalue_reference4 = const_rvalue(); // ok // 規則五:函數類型例外 void fun() {} typedef decltype(fun) FUN; // typedef void FUN(); FUN & lvalue_reference_to_fun = fun; // ok const FUN & const_lvalue_reference_to_fun = fun; // ok FUN && rvalue_reference_to_fun = fun; // ok const FUN && const_rvalue_reference_to_fun = fun; // ok
【說明】:(1) 一些支持右值引用但版本較低的編譯器可能會允許右值引用綁定到左值,例如g++4.4.4就允許,但g++4.6.3就不允許了,clang++3.2也不允許,據說VS2010 beta版允許,正式版就不允許了,本人無VS2010環境,沒測試過。
(2)右值引用綁定到字面值常量同樣符合上述規則,例如:int &&rr = 123;,這裡的字面值123雖然被稱為常量,可它的類型為int,而不是const int。對此C++03標准文檔4.4.1節及其腳注中有如下說明:
If T is a non-class type, the type of the rvalue is the cv-unqualified version of T.
In C++ class rvalues can have cv-qualified types (because they are objects). This differs from ISO C, in which non-lvalues never have cv-qualified types.
因此123是非const右值,int &&rr = 123;語句符合上述規則三。
此,我們已經了解了不少右值引用的知識點了,下面給出了一個完整地利用右值引用實現move語意的例子:
#include#include #define PRINT(msg) do { std::cout << msg << std::endl; } while(0) template struct remove_reference {typedef _Tp type;}; template struct remove_reference<_Tp&> {typedef _Tp type;}; template struct remove_reference<_Tp&&> {typedef _Tp type;}; template inline typename remove_reference<_Tp>::type&& move(_Tp&& __t) { typedef typename remove_reference<_Tp>::type _Up; return static_cast<_Up&&>(__t); } class A { public: A(const char *pstr) { PRINT("constructor"); m_data = (pstr != 0 ? strcpy(new char[strlen(pstr) + 1], pstr) : 0); } A(const A &a) { PRINT("copy constructor"); m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0); } A &operator =(const A &a) { PRINT("copy assigment"); if (this != &a) { delete [] m_data; m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0); } return *this; } A(A &&a) : m_data(a.m_data) { PRINT("move constructor"); a.m_data = 0; } A & operator = (A &&a) { PRINT("move assigment"); if (this != &a) { m_data = a.m_data; a.m_data = 0; } return *this; } ~A() { PRINT("destructor"); delete [] m_data; } private: char * m_data; }; void swap(A &a, A &b) { A tmp(move(a)); a = move(b); b = move(tmp); } int main(int argc, char **argv, char **env) { A a("123"), b("456"); swap(a, b); return 0; }
輸出結果為:
constructor constructor move constructor move assigment move assigment destructor destructor destructor