[C/C++]关于C++11中的std::move和std::forward
http://blog.sina.com.cn/s/blog_53b7ddf00101p5t0.html
std::move是一個(gè)用于提示優(yōu)化的函數(shù),過去的c++98中,由于無法將作為右值的臨時(shí)變量從左值當(dāng)中區(qū)別出來,所以程序運(yùn)行時(shí)有大量臨時(shí)變量白白的創(chuàng)建后又立刻銷毀,其中又尤其是返回字符串std::string的函數(shù)存在最大的浪費(fèi)。
比如:
1 std::string fileContent = “oldContent”;2 s = readFileContent(fileName);
因?yàn)椴⒉皇撬星闆r下,C++編譯器都能進(jìn)行返回值優(yōu)化,所以,向上面的例子中,往往會(huì)創(chuàng)建多個(gè)字符串。readFileContent如果沒有內(nèi)部狀態(tài),那么,它的返回值多半是std::string(const std::string的做法不再被推薦了),而不是const std::string&。這是一個(gè)浪費(fèi),函數(shù)的返回值被拷貝到s中后,棧上的臨時(shí)對(duì)象就被銷毀了。
在C++11中,編碼者可以主動(dòng)提示編譯器,readFileContent返回的對(duì)象是臨時(shí)的,可以被挪作他用:std::move。
將上面的例子改成:
1 std::string fileContent = “oldContent”;2 s = std::move(readFileContent(fileName));
后,對(duì)象s在被賦值的時(shí)候,方法std::string::operator =(std::string&&)會(huì)被調(diào)用,符號(hào)&&告訴std::string類的編寫者,傳入的參數(shù)是一個(gè)臨時(shí)對(duì)象,可以挪用其數(shù)據(jù),于是std::string::operator =(std::string&&)的實(shí)現(xiàn)代碼中,會(huì)置空形參,同時(shí)將原本保存在中形參中的數(shù)據(jù)移動(dòng)到自身。
不光是臨時(shí)變量,只要是你認(rèn)為不再需要的數(shù)據(jù),都可以考慮用std::move移動(dòng)。
比較有名的std::move用法是在swap中:
1 template2 void swap(T& a, T& b)
3 {
4 T t(std::move(a)); // a為空,t占有a的初始數(shù)據(jù)
5 a = std::move(b); // b為空, a占有b的初始數(shù)據(jù)
6 b = std::move(t); // t為空,b占有a的初始數(shù)據(jù)
7 }
總之,std::move是為性能而生的,正式因?yàn)榱擞辛诉@個(gè)主動(dòng)報(bào)告廢棄物的設(shè)施,所以C++11中的STL性能大幅提升,即使C++用戶仍然按找舊有的方式來編碼,仍然能因中新版STL等標(biāo)準(zhǔn)庫的強(qiáng)化中收益。
?
std::forward是用于模板編程中的,如果不需要編寫通用的模板類和函數(shù),可能不怎么用的上它。
要認(rèn)識(shí)它的作用,需要知道C++中的幾條規(guī)則:(這里有篇挺好的文章:http://blog.csdn.net/zwvista/article/details/6848582,但似乎因標(biāo)準(zhǔn)的更新,其中的規(guī)則已不完全成立了)
1.?引用折疊規(guī)則:
X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&
2. 對(duì)于模板函數(shù)中的形參聲明T&&(這里的模板參數(shù)T,最終推演的結(jié)果可能不是一個(gè)純類型,它可能還會(huì)帶有引用/常量修飾符,如,T推演為const int時(shí),實(shí)際形參為const int &&),會(huì)有如下規(guī)則:
如果調(diào)用函數(shù)時(shí)的實(shí)參為U&(這里的U可能有const/volatile修飾,但沒有左/右引用修飾了),那么T推演為U&,顯然根據(jù)上面的引用折疊規(guī)則,U& &&=>U&。
如果調(diào)用實(shí)參為U&&,雖然將T推導(dǎo)為U&&和U都能滿足折疊規(guī)則(U&& &&=> U&&且U &&=>U&&),但標(biāo)準(zhǔn)規(guī)定,這里選擇將T推演為U而非U&&。
總結(jié)一下第2條規(guī)則:當(dāng)形參聲明為T&&時(shí),對(duì)于實(shí)參U&,T被推演為U&;當(dāng)實(shí)參是U&&時(shí),T被推演為U。當(dāng)然,T和U具有相同的const/volatile屬性。
3.這點(diǎn)很重要,也是上面zwvista的文章中沒有提到的:形參T&& t中的變量t,始終是左值引用,即使調(diào)用函數(shù)的實(shí)參是右值引用也不例外。可以這么理解,本來,左值和右值概念的本質(zhì)區(qū)別就是,左值是用戶顯示聲明或分配內(nèi)存的變量,能夠直接用變量名訪問,而右值主要是臨時(shí)變量。當(dāng)一個(gè)臨時(shí)變量傳入形參為T&& t的模板函數(shù)時(shí),T被推演為U,參數(shù)t所引用的臨時(shí)變量因?yàn)殚_始能夠被據(jù)名訪問了,所以它變成了左值。這也就是std::forward存在的原因!當(dāng)你以為實(shí)參是右值所以t也應(yīng)該是右值時(shí),它跟你開了個(gè)玩笑,它是左值!如果你要進(jìn)一步調(diào)用的函數(shù)會(huì)根據(jù)左右值引用性來進(jìn)行不同操作,那么你在將t傳給其他函數(shù)時(shí),應(yīng)該先用std::forward恢復(fù)t的本來引用性,恢復(fù)的依據(jù)是模板參數(shù)T的推演結(jié)果。雖然t的右值引用行會(huì)退化,變成左值引用,但根據(jù)實(shí)參的左右引用性不同,T會(huì)被分別推演為U&和U,這就是依據(jù)!因此傳給std::forward的兩個(gè)參數(shù)一個(gè)都不能少:std::forward(t)。
?
再來,討論一下,一個(gè)模板函數(shù)如果要保留參數(shù)的左右值引用性,為什么應(yīng)該聲明為T&&:
如果聲明函數(shù)f(T t):實(shí)參會(huì)直接進(jìn)行值傳遞,失去了引用性。
如果聲明函數(shù)f(T &t): 根據(jù)引用折疊法則,無論T是U&還是U&&,T&的折疊結(jié)果都只會(huì)是U&,即,這個(gè)聲明不能用于匹配右值引用實(shí)參。
如果聲明函數(shù)f(T &&t): 如果T為U&,T&&的結(jié)果是U&,可以匹配左值實(shí)參;如果T為U&&,T&&的結(jié)果是U&&,可以匹配右值實(shí)參。又因?yàn)門的cv性和U相同,所以這種聲明能夠保留實(shí)參的類型信息。
?
先來看一組幫助類:
1 template struct TypeName { static const char *get(){ return "Type"; } };2 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T> { static const char *get(){ return "const Type"; } };
3 template struct TypeName { static const char *get(){ return "Type&"; } };
4 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T&> { static const char *get(){ return "const Type&"; } };
5 template struct TypeName { static const char *get(){ return "Type&&"; } };
6 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T&&> { static const char *get(){ return "const Type&&"; } };
在模板函數(shù)內(nèi)部將模板參數(shù)T傳給TypeName,就可以訪問T的類型字符串:TypeName::get()。
?
再一個(gè)幫助函數(shù),用于打印一個(gè)表達(dá)式的類型:
1 template2 void printValType(T &&val)
3 {
4 cout << TypeName::get() << endl;
5 }
注意3條規(guī)則在這個(gè)模板函數(shù)上的應(yīng)用。規(guī)則1,解釋了T&& val的聲明足以保留實(shí)參的類型信息。規(guī)則2,說明了,當(dāng)實(shí)參是string&時(shí),T就是string&;當(dāng)實(shí)參是const string&&時(shí),T就是const string(而非const string&&)。規(guī)則3,強(qiáng)調(diào),無論實(shí)參是string&還是string&&,形參val的類型都是string&!
注意TypeName的寫法,因?yàn)門只能為U&或者U,顯然T&&可以根據(jù)折疊法則還原為實(shí)參類型U&和U&&。
?
這里是常見的const/左右引用組合的情形:
1 class A{}; // 測(cè)試類2 A& lRefA() { static A a; return a;} // 左值
3 const A& clRefA() { static A a; return a;} // 常左值
4 A rRefA() { return A(); } // 右值
5 const A crRefA() { return A(); } // 常右值
測(cè)試一下上面的表達(dá)式類型:
1 printValType(lRefA());2 printValType(clRefA());
3 printValType(rRefA());
4 printValType(crRefA());
輸出依次是: Type&,const Type&,Type&&,const Type&&。
?
現(xiàn)在正式來探討std::forward的實(shí)現(xiàn)。
回顧一下使用std::forward的原因:由于聲明為f(T&& t)的模板函數(shù)的形參t會(huì)失去右值引用性質(zhì),所以在將t傳給更深層函數(shù)前,可能會(huì)需要回復(fù)t的正確引用行,當(dāng)然,修改t的引用性辦不到,但根據(jù)t返回另一個(gè)引用還是可以的。恰好,上面的函數(shù)printValType是一個(gè)會(huì)根據(jù)實(shí)參類型不同,作出不同反映的函數(shù),所以可以把它作為f的內(nèi)層函數(shù),來檢測(cè)f有沒有正確的修正t的引用行。
1 template2 void f(T &&a)
3 {
4 printValType(a);
5 }
6
7 int main()
8 {
9 f(lRefA());
10 f(clRefA());
11 f(rRefA());
12 f(crRefA());
13 }
輸出:Type&,const Type&,Type&,const Type&。
可見后兩個(gè)輸出錯(cuò)了,這正是前面規(guī)則3描述的,當(dāng)實(shí)參是右值引用時(shí),雖然T被推演為U,但是參數(shù)a退化成了左值引用。
直接應(yīng)用std::forward:
1 template2 void f(T &&a)
3 {
4 printValType(std::forward(a));
5 }
輸出:Type&,const Type&,Type&&,const Type&&。
輸出正確了,這就是std::forward的作用啊。如果更深層的函數(shù)也需要完整的引用信息,如這里的printValType,那就應(yīng)該在傳遞形參前先std::forward!
?
在編寫自己的forward函數(shù)之前,先來嘗試直接強(qiáng)制轉(zhuǎn)化參數(shù)a:
1 template2 void f(T &&a)
3 {
4 printValType((T&&)a);
5 }
輸出:Type&,const Type&,Type&&,const Type&&。
正確!因?yàn)椴还躎被推演為U&還是U,只要T&&肯定能還原為U&和U&&。
?
考慮下自己的forward函數(shù)應(yīng)該怎么寫:
?因?yàn)樵趂orward的調(diào)用方中,形參已經(jīng)丟失了右值引用信息,唯一的參考依據(jù)是T,要根據(jù)T還原為正確的參數(shù),得T&&,因此,強(qiáng)制轉(zhuǎn)換和返回類型都是T&&了,當(dāng)然,forward還必須被以forward()的方式顯示指定模板類型,這樣才能保證forward的模板參數(shù)T和上層函數(shù)f的T是相同類型。首先:
1 template2 T&& forward(... a)
3 {
4 return (T&&)a;
5 }
調(diào)用方f一定得顯示指定類型forward。
形參怎么寫?形參a的類型由T構(gòu)成,而且forward的實(shí)參一定是左值(暫時(shí)不考慮forward(std::string())的使用方法),也就是說,無論T是U&還是U,形參a的類型一定都得是U&,才能和實(shí)參匹配,所以,結(jié)果是:
1 template2 T&& forward(T& a)
3 {
4 return (T&&)a;
5 }
測(cè)試,輸出:Type&,const Type&,Type&&,const Type&&。
正確!
?
再試下,如果f調(diào)用forward的時(shí)候,使用forward(a)的方式,沒有顯示指定模板類型會(huì)怎么樣:
1 template2 void f(T &&a)
3 {
4 printValType(forward(a));
5 }
輸出:T&&,const Type&&,Type&&,const Type&&。
錯(cuò)了。分析下,因?yàn)閷?shí)參始終是左值,所以forward的形參T& a中,T就被推演為U,因此(T&&)a也就是(U&&)a所以結(jié)果錯(cuò)誤。
為了避免用戶使用forward(a),因此應(yīng)該禁用forward的自動(dòng)模板參數(shù)推演功能!可以借助std::identity,另外,將(T&&)換成static_cast,規(guī)范一下:
1 template2 T&& forward(typename std::identity::type& a)
3 {
4 return static_cast(a);
5 }
?
上面講的是針對(duì)T為U&或U,而實(shí)參始終為左值的情況,這是常見的情形;不過也有實(shí)參為右值的情況,還需要改進(jìn)上面這個(gè)forward,但我這里就不寫了。
這是我手里的gcc4.5.2的forward實(shí)現(xiàn):
1 /// forward (as per N2835)2 /// Forward lvalues as rvalues.
3 template
4 inline typename enable_if::value, _Tp&&>::type
5 forward(typename std::identity<_Tp>::type& __t)
6 { return static_cast<_Tp&&>(__t); }
7
8 /// Forward rvalues as rvalues.
9 template
10 inline typename enable_if::value, _Tp&&>::type
11 forward(typename std::identity<_Tp>::type&& __t)
12 { return static_cast<_Tp&&>(__t); }
13
14 // Forward lvalues as lvalues.
15 template
16 inline typename enable_if::value, _Tp>::type
17 forward(typename std::identity<_Tp>::type __t)
18 { return __t; }
19
20 // Prevent forwarding rvalues as const lvalues.
21 template
22 inline typename enable_if::value, _Tp>::type
23 forward(typename std::remove_reference<_Tp>::type&& __t) = delete;
第1/3版本就相當(dāng)于我之前的實(shí)現(xiàn),而版本2/4是實(shí)參為右值的情況,至于后者這種取舍的原因,還得去自己研究下使用場(chǎng)合和文檔了。
我手里的vc2010實(shí)現(xiàn)的forward和我之前的實(shí)現(xiàn)相同,顯然還不夠,不過vc2010本來對(duì)標(biāo)準(zhǔn)也就還支持得少..
總結(jié)
以上是生活随笔為你收集整理的[C/C++]关于C++11中的std::move和std::forward的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 试管婴儿可以找人代吗
- 下一篇: c,c++中字符串处理函数strtok,