深刻解讀C++中的右值援用。本站提示廣大學習愛好者:(深刻解讀C++中的右值援用)文章只能為提供參考,不一定能成為您想要的結果。以下是深刻解讀C++中的右值援用正文
右值援用(及其支撐的Move語意和完善轉發)是C++0x將要參加的最嚴重說話特征之一,這點從該特征的提案在C++ - State of the Evolution列表上高居榜首也能夠看得出來。
從理論角度講,它可以或許完善處理C++中久長以來為人所诟病的暫時對象效力成績。從說話自己講,它健全了C++中的援用類型在左值右值方面的缺點。從庫設計者的角度講,它給庫設計者又帶來了一把利器。從庫應用者的角度講,不動一兵一卒即可以取得“收費的”效力晉升…
在尺度C++說話中,暫時量(術語為右值,因其湧現在賦值表達式的左邊)可以被傳給函數,但只能被接收為const &類型。如許函數便沒法辨別傳給const &的是真實的右值照樣慣例變量。並且,因為類型為const &,函數也沒法轉變所傳對象的值。C++0x將增長一種名為右值援用的新的援用類型,記作typename &&。這類類型可以被接收為非const值,從而許可轉變其值。這類轉變將許可某些對象創立轉移語義。好比,一個std::vector,就其外部完成而言,是一個C式數組的封裝。假如須要創立vector暫時量或許從函數中前往vector,那就只能經由過程創立一個新的vector並拷貝一切存於右值中的數據來存儲數據。以後這個暫時的vector則會被燒毀,同時刪除其包括的數據。有了右值援用,一個參數為指向某個vector的右值援用的std::vector的轉移結構器就可以夠簡略地將該右值中C式數組的指針復制到新的vector,然後將該右值清空。這裡沒稀有組拷貝,而且燒毀被清空的右值也不會燒毀保留數據的內存。前往vector的函數如今只須要前往一個std::vector<>&&。假如vector沒有轉移結構器,那末成果會像之前一樣:用std::vector<> &參數挪用它的拷貝結構器。假如vector確切具有轉移結構器,那末轉移結構器就會被挪用,從而防止年夜量的內存分派。
一. 界說
平日意義上,在C++中,可取地址,著名字的即為左值。弗成取地址,沒著名字的為右值。右值重要包含字面量,函數前往的暫時變量值,表達式暫時值等。右值援用即為對右值停止援用的類型,在C++98中的援用稱為左值援用。
若有以下類和函數:
class A { private: int* _p; }; A ReturnValue() { return A(); }ReturnValue()的前往值即為右值,它是一個不簽字的暫時變量。在C++98中,只要常量左值援用能力援用這個值。
A& a = ReturnValue(); // error: non-const lvalue reference to type 'A' cannot bind to a temporary of type 'A' const A& a2 = ReturnValue(); // ok經由過程常量左值援用,可以延伸ReturnValue()前往值的性命周期,然則不克不及修正它。C++11的右值援用進場了:
A&& a3 = ReturnValue();右值援用經由過程”&&”來聲明, a3援用了ReturnValue()的前往值,延伸了它的性命周期,而且可以對該暫時值停止修正。
二. 挪動語義
右值援用可以援用並修正右值,然則平日情形下,修正一個暫時值是沒成心義的。但是在對暫時值停止拷貝時,我們可以經由過程右值援用來將暫時值外部的資本移為己用,從而防止了資本的拷貝:
#include<iostream> class A { public: A(int a) :_p(new int(a)) { } // 挪動結構函數 挪動語義 A(A&& rhs) : _p(rhs._p) { // 將暫時值資本置空 防止屢次釋放 如今資本的歸屬權曾經轉移 rhs._p = nullptr; std::cout<<"Move Constructor"<<std::endl; } // 拷貝結構函數 復制語義 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; }
運轉該代碼,發明Move Constructor被挪用(在g++中會對前往值停止優化,不會有任何輸入。可以經由過程-fno-elide-constructors封閉這個選項)。在用右值結構對象時,編譯器會挪用A(A&& rhs)情勢的挪動結構函數,在挪動結構函數中,你可以完成本身的挪動語義,這裡將暫時對象中_p指向內存直接移為己用,防止了資本拷貝。當資本異常年夜或結構異常耗不時,效力晉升將異常顯著。假如A沒有界說挪動結構函數,那末像在C++98中那樣,將挪用拷貝結構函數,履行拷貝語義。挪動不成,還可以拷貝。
std::move:
C++11供給一個函數std::move()來將一個左值強迫轉化為右值:
A a1(5); A a2 = std::move(a1);下面的代碼在結構a2時將會挪用挪動結構函數,而且a1的_p會被置空,由於資本曾經被挪動了。而a1的性命周期和感化域並沒有變,依然要比及main函數停止後再析構,是以以後對a1的_p的拜訪將招致運轉毛病。
斟酌以下代碼:
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; }關於B的挪動結構函數來講,因為rhs是右值,行將被釋放,是以我們不只願望將_pb的資本挪動過去,還願望應用A類的挪動結構函數,將A的資本也履行挪動語義。但是成績出在假如我們直接在初始化列表中應用:_a(rhs._a) 將挪用A的拷貝結構函數。由於參數 rhs._a 此時是一個簽字值,而且可以取址。現實上,B的挪動結構函數的參數rhs也是一個左值,由於它也簽字,而且可取址。這是在C++11右值援用中讓人很困惑的一點:可以接收右值的右值援用自己倒是個左值
三. 完善轉發
假如僅僅為了完成挪動語義,右值援用是沒有需要被提出來的,由於我們在挪用函數時,可以經由過程傳援用的方法來防止暫時值的生成,雖然代碼不是那末直不雅,但效力比應用右值援用只高不低。
右值援用的另外一個感化是完善轉發,完善轉收回如今泛型編程中,將模板函數參數傳遞給該函數挪用的下一個模板函數。如:
template<typename T> void Forward(T t) { Do(t); }下面的代碼中,我們願望Forward函數將傳入參數類型原封不動地傳遞給Do函數,即Forward函數吸收的左值,則Do吸收到左值,Forward吸收到右值,Do也將獲得右值。下面的代碼可以或許准確轉發參數,然則是不完善的,由於Forward吸收參數時履行了一次拷貝。
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的參數停止const重載,便可准確傳遞左值援用。然則當Do函數參數為右值援用時,Forward(5)依然不克不及准確傳遞,由於Forward中的參數都是左值援用。
上面引見在 C++11 中的處理計劃。
PS:援用折疊
C++11引入了援用折疊規矩,聯合右值援用來處理完善轉提問題:
typedef const int T; typedef T& TR; TR& v = 1; // 在C++11中 v的現實類型為 const int&如上代碼中,產生了援用折疊,將TR睜開,獲得 T& & v = 1(留意這裡不是右值援用)。 這裡的 T& + & 被折疊為 T&。更加具體的,依據TR的類型界說,和v的聲明,產生的折疊規矩以下:
T& + & = T& T& + && = T& T&& + & = T& T&& + && = T&&下面的規矩被簡化為:只需湧現左值援用,規矩老是優先折疊為左值援用。僅當湧現兩個右值援用才會折疊為右值援用。
template<typename T> void Forward(T&& t) { Do(static_cast<T&&>(t)); }如許,不管Forward吸收到的是左值,右值,常量,異常量,t都能堅持為其准確類型。
void Forward(X& && t) { Do(static_cast<X& &&>(t)); }折疊後:
void Forward(X& t) { Do(static_cast<X&>(t)); }這裡的static_cast看起來仿佛是沒有需要,而它現實上是為右值援用預備的:
void Forward(X&& && t) { Do(static_cast<X&& &&>(t)); }折疊後:
void Forward(X&& t) { Do(static_cast<X&&>(t)); }後面提到過,可以吸收右值的右值援用自己倒是個左值,由於它簽字而且可以取值。是以在Forward(X&& t)中,參數t曾經是一個左值了,此時我們須要將其轉換為它自己傳入的類型,即為右值。因為static_cast中援用折疊的存在,我們總能復原參數原來的類型。
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; }
四. 附注
左值和左值援用,右值和右值援用都是統一個器械,援用不是一個新的類型,僅僅是一個體名。這一點關於懂得模板推導很主要。關於以下兩個函數
template<typename T> void Fun(T t) { // do something... } template<typename T> void Fun(T& t) { // do otherthing... }
Fun(T t)和Fun(T& t)他們都能接收左值(援用),它們的差別在於對參數作分歧的語義,前者履行拷貝語義,後者只是取個新的別號。是以挪用Fun(a)編譯器會報錯,由於它不曉得你要對a履行何種語義。別的,關於Fun(T t)來講,因為它履行拷貝語義,是以它還能接收右值。是以挪用Fun(5)不會報錯,由於左值援用沒法援用到右值,是以只要Fun(T t)能履行拷貝。
最初,附上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)); }