深入解讀C++中的右值引用
右值引用(及其支持的Move語意和完美轉(zhuǎn)發(fā))是C++0x將要加入的最重大語言特性之一,這點(diǎn)從該特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出來。
從實(shí)踐角度講,它能夠完美解決C++中長久以來為人所詬病的臨時對象效率問題。從語言本身講,它健全了C++中的引用類型在左值右值方面的缺陷。從庫設(shè)計(jì)者的角度講,它給庫設(shè)計(jì)者又帶來了一把利器。從庫使用者的角度講,不動一兵一卒便可以獲得“免費(fèi)的”效率提升…
在標(biāo)準(zhǔn)C++語言中,臨時量(術(shù)語為右值,因其出現(xiàn)在賦值表達(dá)式的右邊)可以被傳給函數(shù),但只能被接受為const &類型。這樣函數(shù)便無法區(qū)分傳給const &的是真實(shí)的右值還是常規(guī)變量。而且,由于類型為const &,函數(shù)也無法改變所傳對象的值。C++0x將增加一種名為右值引用的新的引用類型,記作typename &&。這種類型可以被接受為非const值,從而允許改變其值。這種改變將允許某些對象創(chuàng)建轉(zhuǎn)移語義。比如,一個std::vector,就其內(nèi)部實(shí)現(xiàn)而言,是一個C式數(shù)組的封裝。如果需要創(chuàng)建vector臨時量或者從函數(shù)中返回vector,那就只能通過創(chuàng)建一個新的vector并拷貝所有存于右值中的數(shù)據(jù)來存儲數(shù)據(jù)。之后這個臨時的vector則會被銷毀,同時刪除其包含的數(shù)據(jù)。有了右值引用,一個參數(shù)為指向某個vector的右值引用的std::vector的轉(zhuǎn)移構(gòu)造器就能夠簡單地將該右值中C式數(shù)組的指針復(fù)制到新的vector,然后將該右值清空。這里沒有數(shù)組拷貝,并且銷毀被清空的右值也不會銷毀保存數(shù)據(jù)的內(nèi)存。返回vector的函數(shù)現(xiàn)在只需要返回一個std::vector<>&&。如果vector沒有轉(zhuǎn)移構(gòu)造器,那么結(jié)果會像以前一樣:用std::vector<> &參數(shù)調(diào)用它的拷貝構(gòu)造器。如果vector確實(shí)具有轉(zhuǎn)移構(gòu)造器,那么轉(zhuǎn)移構(gòu)造器就會被調(diào)用,從而避免大量的內(nèi)存分配。
一. 定義
通常意義上,在C++中,可取地址,有名字的即為左值。不可取地址,沒有名字的為右值。右值主要包括字面量,函數(shù)返回的臨時變量值,表達(dá)式臨時值等。右值引用即為對右值進(jìn)行引用的類型,在C++98中的引用稱為左值引用。
如有以下類和函數(shù):
class A { private: int* _p; }; A ReturnValue() { return A(); }
A& a = ReturnValue(); // error: non-const lvalue reference to type 'A' cannot bind to a temporary of type 'A' const A& a2 = ReturnValue(); // ok
A&& a3 = ReturnValue();
二. 移動語義
右值引用可以引用并修改右值,但是通常情況下,修改一個臨時值是沒有意義的。然而在對臨時值進(jìn)行拷貝時,我們可以通過右值引用來將臨時值內(nèi)部的資源移為己用,從而避免了資源的拷貝:
#include<iostream> class A { public: A(int a) :_p(new int(a)) { } // 移動構(gòu)造函數(shù) 移動語義 A(A&& rhs) : _p(rhs._p) { // 將臨時值資源置空 避免多次釋放 現(xiàn)在資源的歸屬權(quán)已經(jīng)轉(zhuǎn)移 rhs._p = nullptr; std::cout<<"Move Constructor"<<std::endl; } // 拷貝構(gòu)造函數(shù) 復(fù)制語義 A(const A& rhs) : _p(new int(*rhs._p)) { std::cout<<"Copy Constructor"<<std::endl; } private: int* _p; }; A ReturnValue() { return A(5); } int main() { A a = ReturnValue(); return 0; }
運(yùn)行該代碼,發(fā)現(xiàn)Move Constructor被調(diào)用(在g++中會對返回值進(jìn)行優(yōu)化,不會有任何輸出??梢酝ㄟ^-fno-elide-constructors關(guān)閉這個選項(xiàng))。在用右值構(gòu)造對象時,編譯器會調(diào)用A(A&& rhs)形式的移動構(gòu)造函數(shù),在移動構(gòu)造函數(shù)中,你可以實(shí)現(xiàn)自己的移動語義,這里將臨時對象中_p指向內(nèi)存直接移為己用,避免了資源拷貝。當(dāng)資源非常大或構(gòu)造非常耗時時,效率提升將非常明顯。如果A沒有定義移動構(gòu)造函數(shù),那么像在C++98中那樣,將調(diào)用拷貝構(gòu)造函數(shù),執(zhí)行拷貝語義。移動不成,還可以拷貝。
std::move:
C++11提供一個函數(shù)std::move()來將一個左值強(qiáng)制轉(zhuǎn)化為右值:
A a1(5); A a2 = std::move(a1);
std::move乍一看沒什么用。它主要用在兩個地方:
- 幫助更好地實(shí)現(xiàn)移動語義
- 實(shí)現(xiàn)完美轉(zhuǎn)發(fā)(下面會提到)
考慮如下代碼:
class B { public: B(B&& rhs) : _pb(rhs._pb) { // how can i move rhs._a to this->_a ? rhs._pb = nullptr; } private: A _a; int * pb; }
這一點(diǎn)在后面的完美轉(zhuǎn)發(fā)還會提到?,F(xiàn)在我們可以用std::move來將rhs._a轉(zhuǎn)換為右值:_a(std::move(rhs._a)),這樣將調(diào)用A的移動構(gòu)造。實(shí)現(xiàn)移動語義。當(dāng)然這里我們確信rhs._a之后不會在使用,因?yàn)閞hs即將被釋放。
三. 完美轉(zhuǎn)發(fā)
如果僅僅為了實(shí)現(xiàn)移動語義,右值引用是沒有必要被提出來的,因?yàn)槲覀冊谡{(diào)用函數(shù)時,可以通過傳引用的方式來避免臨時值的生成,盡管代碼不是那么直觀,但效率比使用右值引用只高不低。
右值引用的另一個作用是完美轉(zhuǎn)發(fā),完美轉(zhuǎn)發(fā)出現(xiàn)在泛型編程中,將模板函數(shù)參數(shù)傳遞給該函數(shù)調(diào)用的下一個模板函數(shù)。如:
template<typename T> void Forward(T t) { Do(t); }
考慮到避免拷貝,我們可以傳遞引用,形如Forward(T& t),但是這種形式的Forward并不能接收右值作為參數(shù),如Forward(5)。因?yàn)榉浅A孔笾挡荒芙壎ǖ接抑???紤]常量左值引用:Forward(const T& t),這種形式的Forward能夠接收任何類型(常量左值引用是萬能引用),但是由于加上了常量修飾符,因此無法正確轉(zhuǎn)發(fā)非常量左值引用:
void Do(int& i) { // do something... } template<typename T> void Forward(const T& t) { Do(t); } int main() { int a = 8; Forward(a); // error. 'void Do(int&)' : cannot convert argument 1 from 'const int' to 'int&' return 0; }
基于這種情況, 我們可以對Forward的參數(shù)進(jìn)行const重載,即可正確傳遞左值引用。但是當(dāng)Do函數(shù)參數(shù)為右值引用時,F(xiàn)orward(5)仍然不能正確傳遞,因?yàn)镕orward中的參數(shù)都是左值引用。
下面介紹在 C++11 中的解決方案。
PS:引用折疊
C++11引入了引用折疊規(guī)則,結(jié)合右值引用來解決完美轉(zhuǎn)發(fā)問題:
typedef const int T; typedef T& TR; TR& v = 1; // 在C++11中 v的實(shí)際類型為 const int&
T& + & = T& T& + && = T& T&& + & = T& T&& + && = T&&
再談轉(zhuǎn)發(fā)
那么上面的引用折疊規(guī)則,對完美轉(zhuǎn)發(fā)有什么用呢?我們注意到,對于T&&類型,它和左值引用折疊為左值引用,和右值引用折疊為右值引用。基于這種特性,我們可以用 T&& 作為我們的轉(zhuǎn)發(fā)函數(shù)模板參數(shù):
template<typename T> void Forward(T&& t) { Do(static_cast<T&&>(t)); }
當(dāng)傳入左值引用 X& 時:
void Forward(X& && t) { Do(static_cast<X& &&>(t)); }
void Forward(X& t) { Do(static_cast<X&>(t)); }
void Forward(X&& && t) { Do(static_cast<X&& &&>(t)); }
void Forward(X&& t) { Do(static_cast<X&&>(t)); }
在C++11中,static_cast<T&&>(t) 可以通過 std::forward<T>(t) 來替代,std::forward是C++11用于實(shí)現(xiàn)完美轉(zhuǎn)發(fā)的一個函數(shù),它和std::move一樣,都通過static_cast來實(shí)現(xiàn)。我們的Forward函數(shù)最終變成了:
template<typename T> void Forward(T&& t) { Do(std::forward<T>(t)); }
#include<iostream> using namespace std; void Do(int& i) { cout << "左值引用" << endl; } void Do(int&& i) { cout << "右值引用" << endl; } void Do(const int& i) { cout << "常量左值引用" << endl; } void Do(const int&& i) { cout << "常量右值引用" << endl; } template<typename T> void PerfectForward(T&& t){ Do(forward<T>(t)); } int main() { int a; const int b; PerfectForward(a); // 左值引用 PerfectForward(move(a)); // 右值引用 PerfectForward(b); // 常量左值引用 PerfectForward(move(b)); // 常量右值引用 return 0; }
四. 附注
左值和左值引用,右值和右值引用都是同一個東西,引用不是一個新的類型,僅僅是一個別名。這一點(diǎn)對于理解模板推導(dǎo)很重要。對于以下兩個函數(shù)
template<typename T> void Fun(T t) { // do something... } template<typename T> void Fun(T& t) { // do otherthing... }
Fun(T t)和Fun(T& t)他們都能接受左值(引用),它們的區(qū)別在于對參數(shù)作不同的語義,前者執(zhí)行拷貝語義,后者只是取個新的別名。因此調(diào)用Fun(a)編譯器會報(bào)錯,因?yàn)樗恢滥阋獙執(zhí)行何種語義。另外,對于Fun(T t)來說,由于它執(zhí)行拷貝語義,因此它還能接受右值。因此調(diào)用Fun(5)不會報(bào)錯,因?yàn)樽笾狄脽o法引用到右值,因此只有Fun(T t)能執(zhí)行拷貝。
最后,附上VS中 std::move 和 std::forward 的源碼:
// move template<class _Ty> inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT { return ((typename remove_reference<_Ty>::type&&)_Arg); } // forward template<class _Ty> inline _Ty&& forward(typename remove_reference<_Ty>::type& _Arg) { // forward an lvalue return (static_cast<_Ty&&>(_Arg)); } template<class _Ty> inline _Ty&& forward(typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT { // forward anything static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call"); return (static_cast<_Ty&&>(_Arg)); }
上一篇:c語言中字符串分割函數(shù)及實(shí)現(xiàn)方法
欄 目:C語言
本文標(biāo)題:深入解讀C++中的右值引用
本文地址:http://mengdiqiu.com.cn/a1/Cyuyan/2264.html
您可能感興趣的文章
- 04-02c語言沒有round函數(shù) round c語言
- 01-10深入理解約瑟夫環(huán)的數(shù)學(xué)優(yōu)化方法
- 01-10深入二叉樹兩個結(jié)點(diǎn)的最低共同父結(jié)點(diǎn)的詳解
- 01-10深入理解C++中常見的關(guān)鍵字含義
- 01-10使用C++實(shí)現(xiàn)全排列算法的方法詳解
- 01-10深入Main函數(shù)中的參數(shù)argc,argv的使用詳解
- 01-10深入第K大數(shù)問題以及算法概要的詳解
- 01-10深入解析最長公共子串
- 01-10c++中inline的用法分析
- 01-10深入理解鏈表的各類操作詳解


閱讀排行
本欄相關(guān)
- 04-02c語言函數(shù)調(diào)用后清空內(nèi)存 c語言調(diào)用
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言的正則匹配函數(shù) c語言正則表達(dá)
- 04-02c語言用函數(shù)寫分段 用c語言表示分段
- 04-02c語言中對數(shù)函數(shù)的表達(dá)式 c語言中對
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段
- 04-02C語言中怎么打出三角函數(shù) c語言中怎
- 04-02c語言調(diào)用函數(shù)求fibo C語言調(diào)用函數(shù)求
隨機(jī)閱讀
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10delphi制作wav文件的方法
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 04-02jquery與jsp,用jquery
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 01-11ajax實(shí)現(xiàn)頁面的局部加載
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10使用C語言求解撲克牌的順子及n個骰子