淺析C++11中的右值引用、轉(zhuǎn)移語義和完美轉(zhuǎn)發(fā)
1. 左值與右值:
C++對于左值和右值沒有標(biāo)準(zhǔn)定義,但是有一個被廣泛認(rèn)同的說法:可以取地址的,有名字的,非臨時的就是左值;不能取地址的,沒有名字的,臨時的就是右值.
可見立即數(shù),函數(shù)返回的值等都是右值;而非匿名對象(包括變量),函數(shù)返回的引用,const
對象等都是左值.
從本質(zhì)上理解,創(chuàng)建和銷毀由編譯器幕后控制的,程序員只能確保在本行代碼有效的,就是右值(包括立即數(shù));而用戶創(chuàng)建的,通過作用域規(guī)則可知其生存期的,就是左值(包括函數(shù)返回的局部變量的引用以及const
對象),例如:
int& foo(){int tmp; return tmp;} int fooo(){int tmp; return tmp;} int a=10; const int b; int& temp=foo();//雖然合法,但temp引用了一個已經(jīng)不存在的對象 int tempp=fooo();
以上代碼中,a,temp和foo()
都是非常量左值,b是常量左值,fooo()
是非常量右值,10是常量右值,有一點要特別注意:返回的引用是左值(可以取地址)!
一般來說,編譯器是不允許對右值進行更改的(因為右值的生存期不由程序員掌握,即使更改了右值也未必可以用),對于內(nèi)置類型對象尤其如此,但C++允許使用右值對象調(diào)用成員函數(shù),雖然允許這樣做,但出于同樣原因,最好不要這么做.
2. 右值引用:
右值引用的表示方法為
Datatype&& variable
右值引用是C++ 11新增的特性,所以C++ 98的引用為左值引用.右值引用用來綁定到右值,綁定到右值以后本來會被銷毀的右值的生存期會延長至與綁定到它的右值引用的生存期,右值引用的存在并不是為了取代左值引用,而是充分利用右值(特別是臨時對象)的建構(gòu)來減少對象建構(gòu)和析構(gòu)操作以達到提高效率的目的,例如對于以下函數(shù):
(Demo是一個類) Demo foo(){ Demo tmp; return tmp; }
在編譯器不進行RVO(return value optimization)優(yōu)化的前提下以下操作:
Demo x=foo();
將會調(diào)用三次構(gòu)造函數(shù)(tmp的,x的,臨時對象的),相應(yīng)的在對象被銷毀時也會調(diào)用三次析構(gòu)函數(shù),而如果采用右值引用的方式:
Demo&& x=foo();
那么就不需要進行x的建構(gòu),本來本來要被銷毀的臨時對象也會由于x的綁定而將生存期延長至和x一樣(可以理解為x賦予了那個臨時對象一個合法地位:一個名字),就需要提高了效率(代價就是tmp需要占據(jù)4字節(jié)空間,但這是微不足道的).
右值引用與左值引用綁定規(guī)則:
常量左值引用可以綁定到常量和非常量左值,常量和非常量右值;
非常量左值引用只能綁定到非常量左值;
非常量右值引用只能綁定到非常量右值(vs2013也可以綁定到常量右值);
常量右值引用只能綁定到常量和非常量右值(非常量右值引用只是為了語義的完整而存在,常量左值引用就可以實現(xiàn)它的作用).
雖然從綁定規(guī)則中可以看出常量左值引用也可以綁定到右值,但顯然不可以改變右值的值,右值引用就可以,從而實現(xiàn)轉(zhuǎn)移語義,因為右值引用通常要改變所綁定的右值,所以被綁定的右值不能為const
.
注意:右值引用是左值!
3. 轉(zhuǎn)移語義(move semantics):
右值引用被引入的目的之一就是實現(xiàn)轉(zhuǎn)移語義,轉(zhuǎn)移語義可以將資源 ( 堆,系統(tǒng)對象等 ) 的所有權(quán)從一個對象(通常是匿名的臨時對象)轉(zhuǎn)移到另一個對象,從而減少對象構(gòu)建及銷毀操作,提高程序效率(這在2的例子中已經(jīng)作了解釋).轉(zhuǎn)移語義與拷貝語義是相對的.從轉(zhuǎn)移語義可以看出,實際上,轉(zhuǎn)移語義并不是新的概念,它實際上已經(jīng)在C++98/03的語言和庫中被使用了,比如在某些情況下拷貝構(gòu)造函數(shù)的省略(copy constructor elision in some contexts),智能指針的拷貝(auto_ptr “copy”),鏈表拼接(list::splice)和容器內(nèi)的置換(swap on containers)等,只是還沒有統(tǒng)一的語法和語義支持
雖然普通的函數(shù)和操作符也可以利用右值引用實現(xiàn)轉(zhuǎn)移語義(如2中的例子),但轉(zhuǎn)移語義通常是通過轉(zhuǎn)移構(gòu)造函數(shù)和轉(zhuǎn)移賦值操作符實現(xiàn)的.轉(zhuǎn)移構(gòu)造函數(shù)的原型為Classname(Typename&&)
,而拷貝構(gòu)造函數(shù)的原型為Classname(const Typename&)
,轉(zhuǎn)移構(gòu)造函數(shù)不會被編譯器自動生成,需要自己定義,只定義轉(zhuǎn)移構(gòu)造函數(shù)也不影響編譯器生成拷貝構(gòu)造函數(shù),如果傳遞的參數(shù)是左值,就調(diào)用拷貝構(gòu)造函數(shù),反之,就調(diào)用轉(zhuǎn)移構(gòu)造函數(shù).
例如:
class Demo{ public: Demo():p(new int[10000]{}; Demo(Demo&& lre):arr(lre.arr),size(lra.size){lre.arr=NULL;}//轉(zhuǎn)移構(gòu)造函數(shù) Demo(const Demo& lre):arr(new int[10000]),size(arr.size){ for(int cou=0;cou<10000;++cou) arr[cou]=lew.arr[cou]; } private: int size; int* arr; }
從以上代碼可以看出,拷貝構(gòu)造函數(shù)在堆中重新開辟了一個大小為10000的int
型數(shù)組,然后每個元素分別拷貝,而轉(zhuǎn)移構(gòu)造函數(shù)則是直接接管參數(shù)的指針?biāo)赶虻馁Y源,效率搞下立判!需要注意的是轉(zhuǎn)移構(gòu)造函數(shù)實參必須是右值,一般是臨時對象,如函數(shù)的返回值等,對于此類臨時對象一般在當(dāng)行代碼之后就被銷毀,而采用轉(zhuǎn)移構(gòu)造函數(shù)可以延長其生命期,可謂是物盡其用,同時有避免了重新開辟數(shù)組.對于上述代碼中的轉(zhuǎn)移構(gòu)造函數(shù),有必要詳細分析一下:
Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}
lre
是一個右值引用,通過它間接訪問實參(臨時對象)的資源來完成資源轉(zhuǎn)移,lre
綁定的對象(必須)是右值,但lre
本身是左值;
因為lre
是函數(shù)的局部對象,”lre.arr=NULL
"必不可少,否則函數(shù)結(jié)尾調(diào)用析構(gòu)函數(shù)銷毀lre
時仍然會將資源釋放,轉(zhuǎn)移的資源還是被系統(tǒng)收回.
4. move()函數(shù)
3中的例子并非萬能,Demo(Demo&& lre)的實參必須是右值,有時候一個左值即將到達生存期,但是仍然想要使用轉(zhuǎn)移語義接管它的資源,這時就需要move
函數(shù).
std::move
函數(shù)定義在標(biāo)準(zhǔn)庫<utility>中,它的作用是將左值強行轉(zhuǎn)化為右值使用,從實現(xiàn)上講,std:move
等同于static_cast<T&&>(lvalue)
,由此看出,被轉(zhuǎn)化的左值本身的生存期和左值屬性并沒有被改變,這類似于const_cast
函數(shù).因此被move
的實參應(yīng)該是即將到達生存期的左值,否則的話可能起到反面效果.
5. 完美轉(zhuǎn)發(fā)(perfect forwarding)
完美轉(zhuǎn)發(fā)指的是將一組實參"完美"地傳遞給形參,完美指的是參數(shù)的const
屬性與左右值屬性不變,例如在進行函數(shù)包裝的時候,func
函數(shù)存在下列重載:
void func(const int); void func(int); void func(int&&);
如果要將它們包裝到一個函數(shù)cover
內(nèi),以實現(xiàn):
void cover(typename para){ func(para); }
使得針對不同實參能在cover
內(nèi)調(diào)用相應(yīng)類型的函數(shù),似乎只能通過對cover
進行函數(shù)重載,這使代碼變得冗繁,另一種方法就是使用函數(shù)模板,但在C++ 11之前,實現(xiàn)該功能的函數(shù)模板只能采用值傳遞,如下:
template<typename T> void cover(T para){ ... func(para); ... }
但如果傳遞的是一個相當(dāng)大的對象,又會造成效率問題,要通過引用傳遞實現(xiàn)形參與實參的完美匹配(包裹const
屬性與左右值屬性的完美匹配),就要使用C++ 11 新引入的引用折疊規(guī)則:
函數(shù)形參 T的類型 推導(dǎo)后的函數(shù)形參
T& A& A&
T& A&& A&
T&& A& A&
T&& A&& A&&
因此,對于前例的函數(shù)包裝要求,采用以下模板就可以解決:
template<typename T> void cover(T&& para){ ... func(static_cast<T &&>(para)); ... }
如果傳入的是左值引用,轉(zhuǎn)發(fā)函數(shù)將被實例化為:
void func(T& && para){ func(static_cast<T& &&>(para)); }
應(yīng)用引用折疊,就為:
void func(T& para){ func(static_cast<T&>(para)); }
如果傳入的是右值引用,轉(zhuǎn)發(fā)函數(shù)將被實例化為:
void func(T&& &¶){ func(static_cast<T&& &&>(para)); }
應(yīng)用引用折疊,就是:
void func(T&& para){ func(static_cast<T&&>(para)); }
對于以上的static_cast<T&&>
,實際上只在para
被推導(dǎo)為右值引用的時候才發(fā)揮作用,由于para
是左值(右值引用是左值),因此需要將它轉(zhuǎn)為右值后再傳入func
內(nèi),C++ 11在<untility>定義了一個std::forward<T>
函數(shù)來實現(xiàn)以上行為,
所以最終版本為
template<typename T> void cover(T&& para){ func(forward(forward<T>(para))); }
std::forward
的實現(xiàn)與static_cast<T&&>(para)
稍有不同
std::forward
函數(shù)的用法為forward<T>(para)
, 若T為左值引用,para
將被轉(zhuǎn)換為T
類型的左值,否則para
將被轉(zhuǎn)換為T
類型右值
總結(jié)
以上就是關(guān)于C++11中右值引用、轉(zhuǎn)移語義和完美轉(zhuǎn)發(fā)的全部內(nèi)容,這篇文章介紹的很詳細,希望對大家的學(xué)習(xí)工作能有所幫助。
相關(guān)文章
C語言數(shù)組和指針,內(nèi)存之間的關(guān)系
這篇文章主要介紹了C語言數(shù)組和指針,內(nèi)存之間的關(guān)系,首先論證一維數(shù)組和一級指針之前的關(guān)系,我們常常使用一級指針指針的方式訪問一維數(shù)組,只有對內(nèi)存的理解到位才能理解它們直接的關(guān)系。需要的小伙伴可以參考一下2022-02-02android studio創(chuàng)建C++項目的實現(xiàn)示例
本文主要介紹了android studio創(chuàng)建C++項目的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06C語言創(chuàng)建數(shù)組實現(xiàn)函數(shù)init,empty,reverse
這篇文章主要介紹了C語言創(chuàng)建數(shù)組實現(xiàn)函數(shù)init,empty,reverse,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07StretchBlt函數(shù)和BitBlt函數(shù)用法案例詳解
這篇文章主要介紹了StretchBlt函數(shù)和BitBlt函數(shù)用法案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08