深入理解右值引用,move语义和完美转发
move語(yǔ)義
最原始的左值和右值定義可以追溯到C語(yǔ)言時(shí)代,左值是可以出現(xiàn)在賦值符的左邊和右邊,然而右值只能出現(xiàn)在賦值符的右邊。在C 里,這種方法作為初步判斷左值或右值還是可以的,但不只是那么準(zhǔn)確了。你要說(shuō)C 中的右值到底是什么,這真的很難給出一個(gè)確切的定義。你可以對(duì)某個(gè)值進(jìn)行取地址運(yùn)算,如果不能得到地址,那么可以認(rèn)為這是個(gè)右值。例如:
int& foo();foo() = 3; //ok, foo() is an lvalue int bar();int a = bar(); // ok, bar() is an rvalue為什么要move語(yǔ)義呢?它可以讓你寫(xiě)出更高效的代碼。看下面代碼:
string?foo();string?name("jack");name?=?foo();第三句賦值會(huì)調(diào)用string的賦值操作符函數(shù),發(fā)生了以下事情:
首先要銷(xiāo)毀name的字符串吧
把foo()返回的臨時(shí)字符串拷貝到name吧
最后還要銷(xiāo)毀foo()返回的臨時(shí)字符串吧
這就顯得很不高效,在C 11之前,你要些的高效點(diǎn),可以是swap交換資源。C 11的move語(yǔ)義就是要做這事,這時(shí)重載move賦值操作符
string&?string::operator=(string&&?rhs);move語(yǔ)義不僅僅用于右值,也用于左值。標(biāo)準(zhǔn)庫(kù)提供了std::move方法,將左值轉(zhuǎn)換成右值。因此,對(duì)于swap函數(shù),我們可以這樣實(shí)現(xiàn):
templatevoid?swap(T&?a,?T&?b){????T?temp(std::move(a));????a?=?std::move(b);????b?=?std::move(temp);}右值引用
string&& 這個(gè)類型就是所謂的右值引用,而把T&稱之為左值引用。注意,不要見(jiàn)到T&&就認(rèn)為是右值引用,例如,下面這個(gè)就不是右值引用:
T&&?foo?=?T();?//右值引用auto&&?bar?=?foo;?//?不是右值引用實(shí)際上,T&&有兩種含義,一種就是常見(jiàn)的右值引用;另一種是即可以是右值引用,也可以是左值引用,Scott Meyers把這種稱為Universal Reference,后來(lái)C 委員把這個(gè)改成forwarding reference,畢竟forwarding reference只在某些特定上下文才出現(xiàn)。
有了右值引用,C 11增加了move構(gòu)造和move賦值。考慮這個(gè)情況:
void?foo(X&&?x){??//?...}那么問(wèn)題來(lái)了,x的類型是右值引用,指向一個(gè)右值,但x本身是左值還是右值呢?
C 11對(duì)此做出了區(qū)分:
Things?that?are?declared?as?rvalue?reference?can?be?lvalues?or?rvalues.?The?distinguishing?criterion?is:?if?it?has?a?name,?then?it?is?an?lvalue.?Otherwise,?it?is?an?rvalue.由此可知,x是個(gè)左值。考慮到派生類的move構(gòu)造,我們因這樣寫(xiě)才正確:
Derived(Derived&&?rhs):base(std::move(rhs)?//std::move不可缺{?...?}有一點(diǎn)必須明白,那就是std::move不管接受的參數(shù)是lvalue,還是rvalue都返回rvalue。因此我們可以給出std::move的實(shí)現(xiàn)如下(很接近于標(biāo)準(zhǔn)實(shí)現(xiàn)):
template?typename?remove_reference::type&&?move(T&&?t)?{????using?RRefType?=?typename?remove_reference::type&&;????return?static_cast(t);}完美轉(zhuǎn)發(fā)
假設(shè)有一個(gè)函數(shù)foo,我們寫(xiě)出如下函數(shù),把接受到的參數(shù)轉(zhuǎn)發(fā)給foo:
templatevoid?fwd(TYPE?t){????foo(t);}我們一個(gè)個(gè)來(lái)分析:
如果TYPE是T的話,假設(shè)foo的參數(shù)引用類型,我會(huì)修改傳進(jìn)來(lái)的參數(shù),那么fwd(t)和foo(t)將導(dǎo)致不一樣的效果。
如果TYPE是T&的話,那么fwd傳一個(gè)右值進(jìn)來(lái),沒(méi)法接受,編譯出錯(cuò)。
如果TYPE是T&,而且重載個(gè)const T&來(lái)接受右值,看似可以,但如果多個(gè)參數(shù)呢,你得來(lái)個(gè)排列組合的重載,因此是不通用的做法。
你很難找到一個(gè)好方法來(lái)實(shí)現(xiàn)它,右值引用的引入解決了這個(gè)問(wèn)題,在這種上下文時(shí),它成為forwarding reference。這就涉及到兩條原則。第一條原則是引用折疊原則:
A&?&?折疊成?A&A&?&&?折疊成?A&A&&?&?折疊成?A&A&&?&&?折疊成?A&&第二條是特殊模板參數(shù)推導(dǎo)原則:
1.如果fwd傳進(jìn)的是個(gè)A類型的左值,那么T被決議為A&。2.如果fwd傳進(jìn)的是個(gè)A類型的右值,那么T被決議為A。
將兩條原則結(jié)合起來(lái),就可以實(shí)現(xiàn)完美轉(zhuǎn)發(fā)。
A x; fwd(x); //推導(dǎo)出fwd(A& &&) 折疊后fwd(A&) A foo();fwd(foo());//推導(dǎo)出fwd(A&& &&) 折疊后 fwd(A&&)std::forward應(yīng)用于forwarding reference,代碼看起來(lái)如下:
templatevoid?fwd(T&&?t){????foo(std::forward(t));}要想展開(kāi)完美轉(zhuǎn)發(fā)的過(guò)程,我們必須寫(xiě)出forward的實(shí)現(xiàn)。接下來(lái)就嘗試forward該如何實(shí)現(xiàn),分析一下,std::forward是條件cast的,T的推導(dǎo)類型取決于傳參給t的是左值還是右值。因此,forward需要做的事情就是當(dāng)且僅當(dāng)右值傳給t時(shí),也就是當(dāng)T推導(dǎo)為非引用類型時(shí),forward需要將t(左值)轉(zhuǎn)成右值。forward可以如下實(shí)現(xiàn):
templateT&&?forward(typename?remove_reference::type&?t){????return?static_cast(t);}現(xiàn)在來(lái)看看完美轉(zhuǎn)發(fā)是怎么工作的,我們預(yù)期當(dāng)傳進(jìn)fwd的參數(shù)是左值,從forward返回的是左值引用;傳進(jìn)的是右值,forward返回的是右值引用。假設(shè)傳給fwd是A類型的左值,那么T被推導(dǎo)為A&:
void?fwd(A&?&&?t){????foo(std::forward(t));}forward實(shí)例化:
A&?&&?forward(typename?remove_reference::type&?t){????return?static_cast(t);}引用折疊后:
A&?forward(A&?t){????return?static_cast(t);}可見(jiàn),符合預(yù)期。再看看傳入fwd是右值時(shí),那么T被推導(dǎo)為A:
void?fwd(A?&&?t){????foo(std::forward(t));}forward實(shí)例化如下:
A&&?forward(typename?remove_reference::type&?t){????return?static_cast(t);}也就是:
A&&?forward(A&?t){????return?static_cast(t);}forward返回右值引用,很好,完全符合預(yù)期。
總結(jié)
以上是生活随笔為你收集整理的深入理解右值引用,move语义和完美转发的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 长春房产备案价格查询官网(长春房产备案)
- 下一篇: 如何优雅地检测类型/表达式有效性?