1、右值引用引入的背景 臨時對象的產生和拷貝所帶來的效率折損,一直是C++所為人诟病的問題。但是C++標准允許編譯器對於臨時對象的產生具有完全的自由度,從而發展出了CopyElision、RVO(包括NRVO)等編譯器優化技術,它們可以防止某些情況下臨時對象產生和拷貝。下面簡單地介紹一下CopyElision、RVO,對此不感興趣的可以直接跳過:
(1)CopyElision
CopyElision技術是為了防止某些不必要的臨時對象產生和拷貝,例如:
代碼如下:
structA{
A(int){}
A(constA&){}
};
Aa=42;
理論上講,上述Aa=42;語句將分三步操作:第一步由42構造一個A類型的臨時對象,第二步以臨時對象為參數拷貝構造a,第三步析構臨時對象。如果A是一個很大的類,那麼它的臨時對象的構造和析構將造成很大的內存開銷。我們只需要一個對象a,為什麼不直接以42為參數直接構造a呢?CopyElision技術正是做了這一優化。
【說明】:你可以在A的拷貝構造函數中加一打印語句,看有沒有調用,如果沒有被調用,那麼恭喜你,你的編譯器支持CopyElision。但是需要說明的是:A的拷貝構造函數雖然沒有被調用,但是它的實現不能沒有訪問權限,不信你將它放在private權限裡試試,編譯器肯定會報錯。
(2)返回值優化(RVO,ReturnValueOptimization)
返回值優化技術也是為了防止某些不必要的臨時對象產生和拷貝,例如:
代碼如下:
structA{
A(int){}
A(constA&){}
};
Aget(){returnA(1);}
Aa=get();
理論上講,上述Aa=get();語句將分別執行:首先get()函數中創建臨時對象(假設為tmp1),然後以tmp1為參數拷貝構造返回值(假設為tmp2),最後再以tmp2為參數拷貝構造a,其中還伴隨著tmp1和tmp2的析構。如果A是一個很大的類,那麼它的臨時對象的構造和析構將造成很大的內存開銷。返回值優化技術正是用來解決此問題的,它可以避免tmp1和tmp2兩個臨時對象的產生和拷貝。
【說明】:a)你可以在A的拷貝構造函數中加一打印語句,看有沒有調用,如果沒有被調用,那麼恭喜你,你的編譯器支持返回值優化。但是需要說明的是:A的拷貝構造函數雖然沒有被調用,但是它的實現不能沒有訪問權限,不信你將它放在private權限裡試試,編譯器肯定會報錯。
b)除了返回值優化,你可能還聽說過一個叫具名返回值優化(NamedReturnValueOptimization,NRVO)的優化技術,從程序員的角度而言,它其實跟RVO同樣的邏輯。只是它的臨時對象具有變量名標識,例如修改上述get()函數為:
代碼如下:
Aget(){
Atmp(1);//#1
//dosomething
returntmp;
}
Aa=get();//#2
想想上述修改後A類型共有幾次對象構造?雖然#1處看起來有一次顯示地構造,#2處看起來也有一次顯示地構造,但如果你的編譯器支持NRVO和CopyElision,你會發現整個Aa=get();語句的執行過程,只有一次A對象的構造。如果你在get()函數return語句前打印tmp變量的地址,在Aa=get();語句後打印a的地址,你會發現兩者地址相同,這就是應用了NRVO技術的結果。
(3)CopyElision、RVO無法避免的臨時對象的產生和拷貝
雖然CopyElision和NVO(包括NRVO)等技術能避免一些臨時對象的產生和拷貝,但某些情況下它們卻發揮不了作用,例如:
代碼如下:
template<typenameT>
voidswap(T&a,T&b){
Ttmp(a);
a=b;
b=tmp;
}
我們只是想交換a和b兩個對象所擁有的數據,但卻不得不使用一個臨時對象tmp備份其中一個對象,如果T類型對象擁有指向(或引用)從堆內存分配的數據,那麼深拷貝所帶來的內存開銷是可以想象的。為此,C++11標准引入了右值引用,使用它可以使臨時對象的拷貝具有move語意,從而可以使臨時對象的拷貝具有淺拷貝般的效率,這樣便可以從一定程度上解決臨時對象的深度拷貝所帶來的效率折損。
2、C++03標准中的左值與右值 要理解右值引用,首先得區分左值(lvalue)和右值(rvalue)。
C++03標准中將表達式分為左值和右值,並且“非左即右”:
Everyexpressioniseitheranlvalueoranrvalue.
區分一個表達式是左值還是右值,最簡便的方法就是看能不能夠對它取地址:如果能,就是左值;否則,就是右值。
【說明】:由於右值引用的引入,C++11標准中對表達式的分類不再是“非左即右”那麼簡單,不過為了簡單地理解,我們暫時只需區分左值右值即可,C++11標准中的分類後面會有描述。
3、右值引用的綁定規則 右值引用(rvaluereference,&&)跟傳統意義上的引用(reference,&)很相似,為了更好地區分它們倆,傳統意義上的引用又被稱為左值引用(lvaluereference)。下面簡單地總結了左值引用和右值引用的綁定規則(函數類型對象會有所例外):
(1)非const左值引用只能綁定到非const左值;
(2)const左值引用可綁定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能綁定到非const右值;
(4)const右值引用可綁定到const右值和非const右值。
測試例子如下:
代碼如下:
structA{A(){}};
Alvalue;//非const左值對象
constAconst_lvalue;//const左值對象
Arvalue(){returnA();}//返回一個非const右值對象
constAconst_rvalue(){returnA();}//返回一個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右值
constA&const_lvalue_reference1=lvalue;//ok
constA&const_lvalue_reference2=const_lvalue;//ok
constA&const_lvalue_reference3=rvalue();//ok
constA&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右值,不能綁定到左值
constA&&const_rvalue_reference1=lvalue;//error
constA&&const_rvalue_reference2=const_lvalue;//error
constA&&const_rvalue_reference3=rvalue();//ok
constA&&const_rvalue_reference4=const_rvalue();//ok
//規則五:函數類型例外
voidfun(){}
typedefdecltype(fun)FUN;//typedefvoidFUN();
FUN&lvalue_reference_to_fun=fun;//ok
constFUN&const_lvalue_reference_to_fun=fun;//ok
FUN&&rvalue_reference_to_fun=fun;//ok
constFUN&&const_rvalue_reference_to_fun=fun;//ok
【說明】:(1)一些支持右值引用但版本較低的編譯器可能會允許右值引用綁定到左值,例如g++4.4.4就允許,但g++4.6.3就不允許了,clang++3.2也不允許,據說VS2010beta版允許,正式版就不允許了,本人無VS2010環境,沒測試過。
(2)右值引用綁定到字面值常量同樣符合上述規則,例如:int&&rr=123;,這裡的字面值123雖然被稱為常量,可它的類型為int,而不是constint。對此C++03標准文檔4.4.1節及其腳注中有如下說明:
IfTisanon-classtype,thetypeofthervalueisthecv-unqualifiedversionofT.
InC++classrvaluescanhavecv-qualifiedtypes(becausetheyareobjects).ThisdiffersfromISOC,inwhichnon-lvaluesneverhavecv-qualifiedtypes.
因此123是非const右值,int&&rr=123;語句符合上述規則三。
4、C++11標准中的表達式分類 右值引用的引入,使得C++11標准中對表達式的分類不再是非左值即右值那麼簡單,下圖為C++11標准中對表達式的分類:
簡單解釋如下:
(1)lvalue仍然是傳統意義上的左值;
(2)xvalue(eXpiringvalue)字面意思可理解為生命周期即將結束的值,它是某些涉及到右值引用的表達式的值(Anxvalueistheresultofcertainkindsofexpressionsinvolvingrvaluereferences),例如:調用一個返回類型為右值引用的函數的返回值就是xvalue。
(3)prvalue(purervalue)字面意思可理解為純右值,也可認為是傳統意義上的右值,例如臨時對象和字面值等。
(4)glvalue(generalizedvalue)廣義的左值,包括傳統的左值和xvalue。
(5)rvalue除了傳統意義上的右值,還包括xvalue。
上述lvalue和prvalue分別跟傳統意義上的左值和右值概念一致,比較明確,而將xvalue描述為『某些涉及到右值引用的表達式的值』,某些是哪些呢?C++11標准給出了四種明確為xvalue的情況:
代碼如下:
[Note:Anexpressionisanxvalueifitis:
--theresultofcallingafunction,whetherimplicitlyorexplicitly,whosereturntypeisanrvaluereferencetoobjecttype,
--acasttoanrvaluereferencetoobjecttype,
--aclassmemberaccessexpressiondesignatinganon-staticdatamemberofnon-referencetypeinwhichtheobjectexpressionisanxvalue,or
--a.*pointer-to-memberexpressioninwhichthefirstoperandisanxvalueandthesecondoperandisapointertodatamember.
Ingeneral,theeffectofthisruleisthatnamedrvaluereferencesaretreatedaslvaluesandunnamedrvaluereferencestoobjectsaretreatedasxvalues;rvaluereferencestofunctionsaretreatedaslvalueswhethernamedornot.--endnote]
[Example:
structA{
intm;
};
A&&operator+(A,A);
A&&f();
Aa;
A&&ar=static_cast<A&&>(a);
Theexpressionsf(),f().m,static_cast<A&&>(a),anda+aarexvalues.Theexpressionarisanlvalue.
--endexample]
簡單地理解就是:具名的右值引用(namedrvaluereference)屬於左值,不具名的右值引用(unamedrvaluereference)就屬於xvalue,而引用函數類型的右值引用不論是否具名都當做左值處理。看個例子更容易理解:
[/code]
Arvalue(){returnA();}
A&&rvalue_reference(){returnA();}
fun();//返回的是不具名的右值引用,屬於xvalue
A&&ra1=rvalue();//ra1是具名右值應用,屬於左值
A&&ra2=ra1;//error,ra1被當做左值對待,因此ra2不能綁定到ra1(不符合規則三)
A&la=ra1;//ok,非const左值引用可綁定到非const左值(符合規則一)
代碼如下:
5、move語意
現在,我們重新顧到1-(3),其中提到move語意,那麼怎樣才能使臨時對象的拷貝具有move語意呢?下面我們以一個類的實現為例:
[code]
classA{
public:
A(constchar*pstr=0){m_data=(pstr!=0?strcpy(newchar[strlen(pstr)+1],pstr):0);}
//copyconstructor
A(constA&a){m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0);}
//copyassigment
A&operator=(constA&a){
if(this!=&a){
delete[]m_data;
m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0);
}
return*this;
}
//moveconstructor
A(A&&a):m_data(a.m_data){a.m_data=0;}
//moveassigment
A&operator=(A&&a){
if(this!=&a){
m_data=a.m_data;
a.m_data=0;
}
return*this;
}
~A(){delete[]m_data;}
private:
char*m_data;
};
從上例可以看到,除了傳統的拷貝構造(copyconstructor)和拷貝賦值(copyassigment),我們還為A類的實現添加了移動拷貝構造(moveconstructor)和移動賦值(moveassigment)。這樣,當我們拷貝一個A類的(右值)臨時對象時,就會使用具有move語意的移動拷貝構造函數,從而避免深拷貝中strcpy()函數的調用;當我們將一個A類的(右值)臨時對象賦值給另一個對象時,就會使用具有move語意的移動賦值,從而避免拷貝賦值中strcpy()函數的調用。這就是所謂的move語意。
6、std::move()函數的實現 了解了move語意,那麼再來看1-(3)中的效率問題:
代碼如下:
template<typenameT>//如果T是classA
voidswap(T&a,T&b){
Ttmp(a);//根據右值引用的綁定規則三可知,這裡不會調用moveconstructor,而會調用copyconstructor
a=b;//根據右值引用的綁定規則三可知,這裡不會調用moveassigment,而會調用copyassigment
b=tmp;//根據右值引用的綁定規則三可知,這裡不會調用moveassigment,而會調用copyassigment
}
從上例可以看到,雖然我們實現了moveconstructor和moveassigment,但是swap()函數的例子中仍然使用的是傳統的copyconstructor和copyassigment。要讓它們真正地使用move語意的拷貝和復制,就該std::move()函數登場了,看下面的例子:
代碼如下:
voidswap(A&a,A&b){
Atmp(std::move(a));//std::move(a)為右值,這裡會調用moveconstructor
a=std::move(b);//std::move(b)為右值,這裡會調用moveassigment
b=std::move(tmp);//std::move(tmp)為右值,這裡會調用moveassigment
}
我們不禁要問:我們通過右值應用的綁定規則三和規則四,知道右值引用不能綁定到左值,可是std::move()函數是如何把上述的左值a、b和tmp變成右值的呢?這就要從std::move()函數的實現說起,其實std::move()函數的實現非常地簡單,下面以libcxx庫中的實現(在<type_trait>頭文件中)為例:
代碼如下:
template<class_Tp>
inlinetypenameremove_reference<_Tp>::type&&move(_Tp&&__t){
typedeftypenameremove_reference<_Tp>::type_Up;
returnstatic_cast<_Up&&>(__t);
}
其中remove_reference的實現如下:
代碼如下:
template<class_Tp>structremove_reference{typedef_Tptype;};
template<class_Tp>structremove_reference<_Tp&>{typedef_Tptype;};
template<class_Tp>structremove_reference<_Tp&&>{typedef_Tptype;};
從move()函數的實現可以看到,move()函數的形參(Parameter)類型為右值引用,它怎麼能綁定到作為實參(Argument)的左值a、b和tmp呢?這不是仍然不符合右值應用的綁定規則三嘛!簡單地說,如果move只是個普通的函數(而不是模板函數),那麼根據右值應用的綁定規則三和規則四可知,它的確不能使用左值作為其實參。但它是個模板函數,牽涉到模板參數推導,就有所不同了。C++11標准文檔14.8.2.1節中,關於模板函數參數的推導描述如下:
Templateargumentdeductionisdonebycomparingeachfunctiontemplateparametertype(callitP)withthetypeofthecorrespondingargumentofthecall(callitA)asdescribedbelow.(14.8.2.1.1)
IfPisareferencetype,thetypereferredtobyPisusedfortypededuction.IfPisanrvaluereferencetoacvunqualifiedtemplateparameterandtheargumentisanlvalue,thetype"lvaluereferencetoA"isusedinplaceofAfortypededuction.(14.8.2.1.3)
大致意思是:模板參數的推導其實就是形參和實參的比較和匹配,如果形參是一個引用類型(如P&),那麼就使用P來做類型推導;如果形參是一個cv-unqualified(沒有const和volatile修飾的)右值引用類型(如P&&),並且實參是一個左值(如類型A的對象),就是用A&來做類型推導(使用A&代替A)。
代碼如下:
template<class_Tp>voidf(_Tp&&){/*dosomething*/}
template<class_Tp>voidg(const_Tp&&){/*dosomething*/}
intx=123;
f(x);//ok,f()模板函數形參為非const非volatile右值引用類型,實參x為int類型左值,使用int&來做參數推導,因此調用f<int&>(int&)
f(456);//ok,實參為右值,調用f<int>(int&&)
g(x);//error,g()函數模板參數為const右值引用類型,會調用g<int>(constint&&),通過右值引用規則四可知道,const右值引用不能綁定到左值,因此會導致編譯錯誤
了解了模板函數參數的推導過程,已經不難理解std::move()函數的實現了,當使用左值(假設其類型為T)作為參數調用std::move()函數時,實際實例化並調用的是std::move<T&>(T&),而其返回類型T&&,這就是move()函數左值變右值的過程(其實左值本身仍是左值,只是被當做右值對待而已,被人“抄了家”,變得一無所有)。
【說明】:C++的始祖BjarneStroustrup說:如果move()函數改名為rval()可能會更好些,但是move()這個名字已經被使用了好些年了(Maybeitwouldhavebeenbetterifmove()hadbeencalledrval(),butbynowmove()hasbeenusedforyears.)。
7、完整的示例 至此,我們已經了解了不少右值引用的知識點了,下面給出了一個完整地利用右值引用實現move語意的例子:
代碼如下:
#include<iostream>
#include<cstring>
#definePRINT(msg)do{std::cout<<msg<<std::endl;}while(0)
template<class_Tp>structremove_reference{typedef_Tptype;};
template<class_Tp>structremove_reference<_Tp&>{typedef_Tptype;};
template<class_Tp>structremove_reference<_Tp&&>{typedef_Tptype;};
template<class_Tp>
inlinetypenameremove_reference<_Tp>::type&&move(_Tp&&__t){
typedeftypenameremove_reference<_Tp>::type_Up;
returnstatic_cast<_Up&&>(__t);
}
classA{
public:
A(constchar*pstr){
PRINT("constructor");
m_data=(pstr!=0?strcpy(newchar[strlen(pstr)+1],pstr):0);
}
A(constA&a){
PRINT("copyconstructor");
m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0);
}
A&operator=(constA&a){
PRINT("copyassigment");
if(this!=&a){
delete[]m_data;
m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0);
}
return*this;
}
A(A&&a):m_data(a.m_data){
PRINT("moveconstructor");
a.m_data=0;
}
A&operator=(A&&a){
PRINT("moveassigment");
if(this!=&a){
m_data=a.m_data;
a.m_data=0;
}
return*this;
}
~A(){PRINT("destructor");delete[]m_data;}
private:
char*m_data;
};
voidswap(A&a,A&b){
Atmp(move(a));
a=move(b);
b=move(tmp);
}
intmain(intargc,char**argv,char**env){
Aa("123"),b("456");
swap(a,b);
return0;
}
輸出結果為:
代碼如下:
constructor
constructor
moveconstructor
moveassigment
moveassigment
destructor
destructor
destructor
8、幕後花絮 C++11標准引入右值引用的提案是由HowardHinnant提出的,它的最初提案N1377在02年就提出來了,中間經歷了多次修改N1385、N1690、N1770、N1855、N1952、N2118。包括它的最終版本N2118在內,HowardHinnant的提案中都使用了右值引用直接綁定到左值的例子,並且由HowardHinnant、BjarneStroustrup和BronekKozicki三人08年10月共同署名的《ABriefIntroductiontoRvalueReferences》文章中也有右值引用直接綁定到左值的例子,但奇怪的是11年公布的最新的C++11標准文檔中卻不允許右值引用直接綁定到左值,其中的原因不得而知,但從中不難理解為什麼早些編譯器版本(如g++4.4.4)對右值引用綁定到左值,不會報出編譯錯誤,而最新的編譯器卻會報錯。
另外,HowardHinnant是C++標准委員會LibraryWorkingGroup老大(chairman),libcxx和libcxxabi的維護者,蘋果公司的高級軟件工程師。