轉(zhuǎn)載:http://www.reibang.com/p/d19fc8447eaa
c++中引入了右值引用和移動語義硼啤,可以避免無謂的復(fù)制囱稽,提高程序性能肝谭。有點(diǎn)難理解,于是花時間整理一下自己的理解命贴。
左值咧织、右值
C++中所有的值都必然屬于左值嗓袱、右值二者之一。左值是指表達(dá)式結(jié)束后依然存在的持久化對象习绢,右值是指表達(dá)式結(jié)束時就不再存在的臨時對象渠抹。所有的具名變量或者對象都是左值,而右值不具名闪萄。很難得到左值和右值的真正定義梧却,但是有一個可以區(qū)分左值和右值的便捷方法:看能不能對表達(dá)式取地址,如果能败去,則為左值放航,否則為右值。
看見書上又將右值分為將亡值和純右值圆裕。純右值就是c++98標(biāo)準(zhǔn)中右值的概念广鳍,如非引用返回的函數(shù)返回的臨時變量值;一些運(yùn)算表達(dá)式吓妆,如1+2產(chǎn)生的臨時變量赊时;不跟對象關(guān)聯(lián)的字面量值,如2行拢,'c'祖秒,true,"hello"舟奠;這些值都不能夠被取地址狈涮。
而將亡值則是c++11新增的和右值引用相關(guān)的表達(dá)式,這樣的表達(dá)式通常時將要移動的對象鸭栖、T&&函數(shù)返回值歌馍、std::move()函數(shù)的返回值等,
不懂將亡值和純右值的區(qū)別其實沒關(guān)系晕鹊,統(tǒng)一看作右值即可松却,不影響使用暴浦。
示例:
```python
inti=0;// i是左值, 0是右值
class A {
? public:
? ? ?inta;
?};
A getTemp() {
? ?returnA();
}
A a = getTemp(); // a是左值? getTemp()的返回值是右值(臨時變量)
```
左值引用晓锻、右值引用
c++98中的引用很常見了歌焦,就是給變量取了個別名,在c++11中砚哆,因為增加了右值引用(rvalue reference)的概念独撇,所以c++98中的引用都稱為了左值引用(lvalue reference)。
```python
inta=10;
int& refA = a; // refA是a的別名躁锁, 修改refA就是修改a, a是左值纷铣,左移是左值引用
int&b=1;//編譯錯誤! 1是右值,不能夠使用左值引用
```
c++11中的右值引用使用的符號是&&战转,如
```python
int&& a = 1;//實質(zhì)上就是將不具名(匿名)變量取了個別名
int b = 1;
int&& c = b;? //編譯錯誤搜立! 不能將一個左值復(fù)制給一個右值引用
class A {
? ?public:
? ? ? inta;
? };
A getTemp() {?
? returnA();
}
A&& a = getTemp(); // getTemp()的返回值是右值(臨時變量)
```
getTemp()返回的右值本來在表達(dá)式語句結(jié)束后,其生命也就該終結(jié)了(因為是臨時變量)槐秧,而通過右值引用啄踊,該右值又重獲新生,其生命期將與右值引用類型變量a的生命期一樣刁标,只要a還活著颠通,該右值臨時變量將會一直存活下去。實際上就是給那個臨時變量取了個名字膀懈。
注意:這里a的類型是右值引用類型(int &&)蒜哀,但是如果從左值和右值的角度區(qū)分它,它實際上是個左值吏砂。因為可以對它取地址狐血,而且它還有名字,是一個已經(jīng)命名的右值匈织。
所以牡直,左值引用只能綁定左值,右值引用只能綁定右值碰逸,如果綁定的不對,編譯就會失敗饵史。但是满钟,常量左值引用卻是個奇葩胜榔,它可以算是一個“萬能”的引用類型,它可以綁定非常量左值湃番、常量左值夭织、右值,而且在綁定右值的時候吠撮,常量左值引用還可以像右值引用一樣將右值的生命期延長尊惰,缺點(diǎn)是,只能讀不能改泥兰。
```python
const int& a = 1; //常量左值引用綁定 右值弄屡, 不會報錯
class A {?
? ?public:
? ? ? inta;
};
A getTemp() {
? returnA();
}
const A& a = getTemp(); //不會報錯 而 A& a 會報錯
```
事實上,很多情況下我們用來常量左值引用的這個功能卻沒有意識到逾条,如下面的例子:
```python
#include<iostream>
using namespace std;
class Copyable {
? public:
? ? Copyable() {}
? ? Copyable(const Copyable& o) {
? ? ? ?cout << "Copied" << endl;
? ?}
?};
Copyable ReturnRvalue() {?
? ?return Copyable(); //返回一個臨時對象
}
void AcceptVal (Copyable a) {}
void AcceptRef (const Copyable& a) {}
int main() {
? ?cout << "pass by value: "<< endl;
? ?AcceptVal(ReturnRvalue());// 應(yīng)該調(diào)用兩次拷貝構(gòu)造函數(shù)
? cout << "pass by reference: " << endl;
? AcceptRef(ReturnRvalue()); //應(yīng)該只調(diào)用一次拷貝構(gòu)造函數(shù)
}
```
當(dāng)我敲完上面的例子并運(yùn)行后琢岩,發(fā)現(xiàn)結(jié)果和我想象的完全不一樣!期望中AcceptVal(ReturnRvalue())需要調(diào)用兩次拷貝構(gòu)造函數(shù)师脂,一次在ReturnRvalue()函數(shù)中担孔,構(gòu)造好了Copyable對象,返回的時候會調(diào)用拷貝構(gòu)造函數(shù)生成一個臨時對象吃警,在調(diào)用AcceptVal()時糕篇,又會將這個對象拷貝給函數(shù)的局部變量a,一共調(diào)用了兩次拷貝構(gòu)造函數(shù)酌心。而AcceptRef()的不同在于形參是常量左值引用拌消,它能夠接收一個右值,而且不需要拷貝安券。
而實際的結(jié)果是墩崩,不管哪種方式,一次拷貝構(gòu)造函數(shù)都沒有調(diào)用侯勉!
這是由于編譯器默認(rèn)開啟了返回值優(yōu)化(RVO/NRVO, RVO, Return Value Optimization 返回值優(yōu)化,或者NRVO铐拐, Named Return Value Optimization)遍蟋。編譯器很聰明螟凭,發(fā)現(xiàn)在ReturnRvalue內(nèi)部生成了一個對象螺男,返回之后還需要生成一個臨時對象調(diào)用拷貝構(gòu)造函數(shù),很麻煩绊谭,所以直接優(yōu)化成了1個對象對象,避免拷貝篙耗,而這個臨時變量又被賦值給了函數(shù)的形參宗弯,還是沒必要搂妻,所以最后這三個變量都用一個變量替代了欲主,不需要調(diào)用拷貝構(gòu)造函數(shù)扁瓢。
雖然各大廠家的編譯器都已經(jīng)都有了這個優(yōu)化引几,但是這并不是c++標(biāo)準(zhǔn)規(guī)定的,而且不是所有的返回值都能夠被優(yōu)化敞掘,而這篇文章的主要講的右值引用玖雁,移動語義可以解決編譯器無法解決的問題疯潭。
為了更好的觀察結(jié)果竖哩,可以在編譯的時候加上-fno-elide-constructors選項(關(guān)閉返回值優(yōu)化)。
// g++ test.cpp -o test -fno-elide-constructorspass by value:CopiedCopied//可以看到確實調(diào)用了兩次拷貝構(gòu)造函數(shù)pass by reference:Copied
上面這個例子本意是想說明常量左值引用能夠綁定一個右值遵绰,可以減少一次拷貝(使用非常量的左值引用會編譯失敶环谩)成玫,但是順便講到了編譯器的返回值優(yōu)化哭当。钦勘。編譯器還是干了很多事情的彻采,很有用,但不能過于依賴柴梆,因為你也不確定它什么時候優(yōu)化了什么時候沒優(yōu)化绍在。
總結(jié)一下偿渡,其中T是一個具體類型:
左值引用溜宽, 使用T&, 只能綁定左值
右值引用适揉, 使用T&&嫉嘀, 只能綁定右值
常量左值剪侮, 使用const T&, 既可以綁定左值又可以綁定右值
已命名的右值引用瓣俯,編譯器會認(rèn)為是個左值
編譯器有返回值優(yōu)化,但不要過于依賴
移動構(gòu)造和移動賦值
回顧一下如何用c++實現(xiàn)一個字符串類MyString腔剂,MyString內(nèi)部管理一個C語言的char *數(shù)組推掸,這個時候一般都需要實現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值函數(shù)谅畅,因為默認(rèn)的拷貝是淺拷貝毡泻,而指針這種資源不能共享仇味,不然一個析構(gòu)了丹墨,另一個也就完蛋了贩挣。
具體代碼如下:
#include<iostream>#include<cstring>#include<vector>usingnamespacestd;classMyString{public:staticsize_t CCtor;//統(tǒng)計調(diào)用拷貝構(gòu)造函數(shù)的次數(shù)//? ? static size_t CCtor; //統(tǒng)計調(diào)用拷貝構(gòu)造函數(shù)的次數(shù)public:// 構(gòu)造函數(shù)MyString(constchar*cstr=0){if(cstr){m_data=newchar[strlen(cstr)+1];strcpy(m_data,cstr);}else{m_data=newchar[1];*m_data='\0';}}// 拷貝構(gòu)造函數(shù)MyString(constMyString&str){CCtor++;m_data=newchar[strlen(str.m_data)+1];strcpy(m_data,str.m_data);}// 拷貝賦值函數(shù) =號重載MyString&operator=(constMyString&str){if(this==&str)// 避免自我賦值!!return*this;delete[]m_data;m_data=newchar[strlen(str.m_data)+1];strcpy(m_data,str.m_data);return*this;}~MyString(){delete[]m_data;}char*get_c_str()const{returnm_data;}private:char*m_data;};size_t MyString::CCtor=0;intmain(){vector<MyString>vecStr;vecStr.reserve(1000);//先分配好1000個空間王财,不這么做绒净,調(diào)用的次數(shù)可能遠(yuǎn)大于1000for(inti=0;i<1000;i++){vecStr.push_back(MyString("hello"));}cout<<MyString::CCtor<<endl;}
代碼看起來挺不錯挂疆,卻發(fā)現(xiàn)執(zhí)行了1000次拷貝構(gòu)造函數(shù)缤言,如果MyString("hello")構(gòu)造出來的字符串本來就很長墨闲,構(gòu)造一遍就很耗時了鸳碧,最后卻還要拷貝一遍,而MyString("hello")只是臨時對象腾仅,拷貝完就沒什么用了推励,這就造成了沒有意義的資源申請和釋放操作肉迫,如果能夠直接使用臨時對象已經(jīng)申請的資源喊衫,既能節(jié)省資源族购,又能節(jié)省資源申請和釋放的時間寝杖。而C++11新增加的移動語義就能夠做到這一點(diǎn)瑟幕。
要實現(xiàn)移動語義就必須增加兩個函數(shù):移動構(gòu)造函數(shù)和移動賦值構(gòu)造函數(shù)收苏。
#include<iostream>#include<cstring>#include<vector>usingnamespacestd;classMyString{public:staticsize_t CCtor;//統(tǒng)計調(diào)用拷貝構(gòu)造函數(shù)的次數(shù)staticsize_t MCtor;//統(tǒng)計調(diào)用移動構(gòu)造函數(shù)的次數(shù)staticsize_t CAsgn;//統(tǒng)計調(diào)用拷貝賦值函數(shù)的次數(shù)staticsize_t MAsgn;//統(tǒng)計調(diào)用移動賦值函數(shù)的次數(shù)public:// 構(gòu)造函數(shù)MyString(constchar*cstr=0){if(cstr){m_data=newchar[strlen(cstr)+1];strcpy(m_data,cstr);}else{m_data=newchar[1];*m_data='\0';}}// 拷貝構(gòu)造函數(shù)MyString(constMyString&str){CCtor++;m_data=newchar[strlen(str.m_data)+1];strcpy(m_data,str.m_data);}// 移動構(gòu)造函數(shù)MyString(MyString&&str)noexcept:m_data(str.m_data){MCtor++;str.m_data=nullptr;//不再指向之前的資源了}// 拷貝賦值函數(shù) =號重載MyString&operator=(constMyString&str){CAsgn++;if(this==&str)// 避免自我賦值!!return*this;delete[]m_data;m_data=newchar[strlen(str.m_data)+1];strcpy(m_data,str.m_data);return*this;}// 移動賦值函數(shù) =號重載MyString&operator=(MyString&&str)noexcept{MAsgn++;if(this==&str)// 避免自我賦值!!return*this;delete[]m_data;m_data=str.m_data;str.m_data=nullptr;//不再指向之前的資源了return*this;}~MyString(){delete[]m_data;}char*get_c_str()const{returnm_data;}private:char*m_data;};size_t MyString::CCtor=0;size_t MyString::MCtor=0;size_t MyString::CAsgn=0;size_t MyString::MAsgn=0;intmain(){vector<MyString>vecStr;vecStr.reserve(1000);//先分配好1000個空間for(inti=0;i<1000;i++){vecStr.push_back(MyString("hello"));}cout<<"CCtor = "<<MyString::CCtor<<endl;cout<<"MCtor = "<<MyString::MCtor<<endl;cout<<"CAsgn = "<<MyString::CAsgn<<endl;cout<<"MAsgn = "<<MyString::MAsgn<<endl;}/* 結(jié)果
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/
可以看到排吴,移動構(gòu)造函數(shù)與拷貝構(gòu)造函數(shù)的區(qū)別是钻哩,拷貝構(gòu)造的參數(shù)是const MyString& str街氢,是常量左值引用珊肃,而移動構(gòu)造的參數(shù)是MyString&& str伦乔,是右值引用,而MyString("hello")是個臨時對象爱只,是個右值恬试,優(yōu)先進(jìn)入移動構(gòu)造函數(shù)而不是拷貝構(gòu)造函數(shù)训柴。而移動構(gòu)造函數(shù)與拷貝構(gòu)造不同畦粮,它并不是重新分配一塊新的空間宣赔,將要拷貝的對象復(fù)制過來儒将,而是"偷"了過來钩蚊,將自己的指針指向別人的資源蹈矮,然后將別人的指針修改為nullptr泛鸟,這一步很重要北滥,如果不將別人的指針修改為空再芋,那么臨時對象析構(gòu)的時候就會釋放掉這個資源济赎,"偷"也白偷了。下面這張圖可以解釋copy和move的區(qū)別构捡。
copy和move的區(qū)別.png
不用奇怪為什么可以搶別人的資源,臨時對象的資源不好好利用也是浪費(fèi)蓖谢,因為生命周期本來就是很短闪幽,在你執(zhí)行完這個表達(dá)式之后盯腌,它就毀滅了腕够,充分利用資源帚湘,才能很高效大诸。
對于一個左值,肯定是調(diào)用拷貝構(gòu)造函數(shù)了焙贷,但是有些左值是局部變量辙芍,生命周期也很短沸手,能不能也移動而不是拷貝呢契吉?C++11為了解決這個問題捐晶,提供了std::move()方法來將左值轉(zhuǎn)換為右值,從而方便應(yīng)用移動語義山上。我覺得它其實就是告訴編譯器佩憾,雖然我是一個左值妄帘,但是不要對我用拷貝構(gòu)造函數(shù)抡驼,而是用移動構(gòu)造函數(shù)吧致盟。馏锡。伟端。
intmain(){vector<MyString>vecStr;vecStr.reserve(1000);//先分配好1000個空間for(inti=0;i<1000;i++){MyStringtmp("hello");vecStr.push_back(tmp);//調(diào)用的是拷貝構(gòu)造函數(shù)}cout<<"CCtor = "<<MyString::CCtor<<endl;cout<<"MCtor = "<<MyString::MCtor<<endl;cout<<"CAsgn = "<<MyString::CAsgn<<endl;cout<<"MAsgn = "<<MyString::MAsgn<<endl;cout<<endl;MyString::CCtor=0;MyString::MCtor=0;MyString::CAsgn=0;MyString::MAsgn=0;vector<MyString>vecStr2;vecStr2.reserve(1000);//先分配好1000個空間for(inti=0;i<1000;i++){MyStringtmp("hello");vecStr2.push_back(std::move(tmp));//調(diào)用的是移動構(gòu)造函數(shù)}cout<<"CCtor = "<<MyString::CCtor<<endl;cout<<"MCtor = "<<MyString::MCtor<<endl;cout<<"CAsgn = "<<MyString::CAsgn<<endl;cout<<"MAsgn = "<<MyString::MAsgn<<endl;}/* 運(yùn)行結(jié)果
CCtor = 1000
MCtor = 0
CAsgn = 0
MAsgn = 0
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/
下面再舉幾個例子:
MyStringstr1("hello");//調(diào)用構(gòu)造函數(shù)MyStringstr2("world");//調(diào)用構(gòu)造函數(shù)MyStringstr3(str1);//調(diào)用拷貝構(gòu)造函數(shù)MyStringstr4(std::move(str1));// 調(diào)用移動構(gòu)造函數(shù)蕉饼、//? ? cout << str1.get_c_str() << endl; // 此時str1的內(nèi)部指針已經(jīng)失效了昧港!不要使用//注意:雖然str1中的m_dat已經(jīng)稱為了空支子,但是str1這個對象還活著值朋,知道出了它的作用域才會析構(gòu)!而不是move完了立刻析構(gòu)MyString str5;str5=str2;//調(diào)用拷貝賦值函數(shù)MyString str6;str6=std::move(str2);// str2的內(nèi)容也失效了趾代,不要再使用
需要注意一下幾點(diǎn):
str6 = std::move(str2)撒强,雖然將str2的資源給了str6,但是str2并沒有立刻析構(gòu)胚想,只有在str2離開了自己的作用域的時候才會析構(gòu)浊服,所以牙躺,如果繼續(xù)使用str2的m_data變量,可能會發(fā)生意想不到的錯誤惩淳。
如果我們沒有提供移動構(gòu)造函數(shù)思犁,只提供了拷貝構(gòu)造函數(shù)激蹲,std::move()會失效但是不會發(fā)生錯誤学辱,因為編譯器找不到移動構(gòu)造函數(shù)就去尋找拷貝構(gòu)造函數(shù),也這是拷貝構(gòu)造函數(shù)的參數(shù)是const T&常量左值引用的原因衙傀!
c++11中的所有容器都實現(xiàn)了move語義统抬,move只是轉(zhuǎn)移了資源的控制權(quán)聪建,本質(zhì)上是將左值強(qiáng)制轉(zhuǎn)化為右值使用金麸,以用于移動拷貝或賦值钱骂,避免對含有資源的對象發(fā)生無謂的拷貝。move對于擁有如內(nèi)存愉烙、文件句柄等資源的成員的對象有效步责,如果是一些基本類型蔓肯,如int和char[10]數(shù)組等振乏,如果使用move慧邮,仍會發(fā)生拷貝(因為沒有對應(yīng)的移動構(gòu)造函數(shù))误澳,所以說move對含有資源的對象說更有意義。
universal references(通用引用)
當(dāng)右值引用和模板結(jié)合的時候裆装,就復(fù)雜了哨免。T&&并不一定表示右值引用铁瞒,它可能是個左值引用又可能是個右值引用慧耍。例如:
template<typenameT>voidf(T&¶m){}f(10);//10是右值intx=10;//f(x);//x是左值
如果上面的函數(shù)模板表示的是右值引用的話芍碧,肯定是不能傳遞左值的泌豆,但是事實卻是可以吏饿。這里的&&是一個未定義的引用類型,稱為universal references畴博,它必須被初始化俱病,它是左值引用還是右值引用卻決于它的初始化亮隙,如果它被一個左值初始化溢吻,它就是一個左值引用促王;如果被一個右值初始化硼砰,它就是一個右值引用。
注意:只有當(dāng)發(fā)生自動類型推斷時(如函數(shù)模板的類型自動推導(dǎo)恶阴,或auto關(guān)鍵字)焦匈,&&才是一個universal references昵仅。
例如:
template<typenameT>voidf(T&¶m);//這里T的類型需要推導(dǎo)摔笤,所以&&是一個 universal referencestemplate<typenameT>classTest{Test(Test&&rhs);//Test是一個特定的類型彰触,不需要類型推導(dǎo)况毅,所以&&表示右值引用? };voidf(Test&¶m);//右值引用//復(fù)雜一點(diǎn)template<typenameT>voidf(std::vector<T>&¶m);//在調(diào)用這個函數(shù)之前,這個vector<T>中的推斷類型//已經(jīng)確定了么鹤,所以調(diào)用f函數(shù)的時候沒有類型推斷了,所以是 右值引用template<typenameT>voidf(constT&¶m);//右值引用// universal references僅僅發(fā)生在 T&& 下面迅皇,任何一點(diǎn)附加條件都會使之失效
所以最終還是要看T被推導(dǎo)成什么類型登颓,如果T被推導(dǎo)成了string框咙,那么T&&就是string&&痢甘,是個右值引用塞栅,如果T被推導(dǎo)為string&作烟,就會發(fā)生類似string& &&的情況拿撩,對于這種情況,c++11增加了引用折疊的規(guī)則如蚜,總結(jié)如下:
所有的右值引用疊加到右值引用上仍然使一個右值引用压恒。
所有的其他引用類型之間的疊加都將變成左值引用。
如上面的T& &&其實就被折疊成了個string &错邦,是一個左值引用探赫。
#include<iostream>#include<type_traits>#include<string>usingnamespacestd;template<typenameT>voidf(T&¶m){if(std::is_same<string,T>::value)std::cout<<"string"<<std::endl;elseif(std::is_same<string&,T>::value)std::cout<<"string&"<<std::endl;elseif(std::is_same<string&&,T>::value)std::cout<<"string&&"<<std::endl;elseif(std::is_same<int,T>::value)std::cout<<"int"<<std::endl;elseif(std::is_same<int&,T>::value)std::cout<<"int&"<<std::endl;elseif(std::is_same<int&&,T>::value)std::cout<<"int&&"<<std::endl;elsestd::cout<<"unkown"<<std::endl;}intmain(){intx=1;f(1);// 參數(shù)是右值 T推導(dǎo)成了int, 所以是int&& param, 右值引用f(x);// 參數(shù)是左值 T推導(dǎo)成了int&, 所以是int&&& param, 折疊成 int&,左值引用int&&a=2;f(a);//雖然a是右值引用,但它還是一個左值兴猩, T推導(dǎo)成了int&string str="hello";f(str);//參數(shù)是左值 T推導(dǎo)成了string&f(string("hello"));//參數(shù)是右值期吓, T推導(dǎo)成了stringf(std::move(str));//參數(shù)是右值, T推導(dǎo)成了string}
所以,歸納一下, 傳遞左值進(jìn)去,就是左值引用屉来,傳遞右值進(jìn)去蝶桶,就是右值引用。如它的名字,這種類型確實很"通用"脂信,下面要講的完美轉(zhuǎn)發(fā)蹬铺,就利用了這個特性琐馆。
完美轉(zhuǎn)發(fā)
所謂轉(zhuǎn)發(fā),就是通過一個函數(shù)將參數(shù)繼續(xù)轉(zhuǎn)交給另一個函數(shù)進(jìn)行處理喊巍,原參數(shù)可能是右值款咖,可能是左值,如果還能繼續(xù)保持參數(shù)的原有特征,那么它就是完美的。
voidprocess(int&i){cout<<"process(int&):"<<i<<endl;}voidprocess(int&&i){cout<<"process(int&&):"<<i<<endl;}voidmyforward(int&&i){cout<<"myforward(int&&):"<<i<<endl;process(i);}intmain(){inta=0;process(a);//a被視為左值 process(int&):0process(1);//1被視為右值 process(int&&):1process(move(a));//強(qiáng)制將a由左值改為右值 process(int&&):0myforward(2);//右值經(jīng)過forward函數(shù)轉(zhuǎn)交給process函數(shù),卻稱為了一個左值,//原因是該右值有了名字? 所以是 process(int&):2myforward(move(a));// 同上秽晚,在轉(zhuǎn)發(fā)的時候右值變成了左值? process(int&):0// forward(a) // 錯誤用法巢掺,右值引用不接受左值}
上面的例子就是不完美轉(zhuǎn)發(fā),而c++中提供了一個std::forward()模板函數(shù)解決這個問題鳄袍。將上面的myforward()函數(shù)簡單改寫一下:
voidmyforward(int&&i){cout<<"myforward(int&&):"<<i<<endl;process(std::forward<int>(i));}myforward(2);// process(int&&):2
上面修改過后還是不完美轉(zhuǎn)發(fā)哀九,myforward()函數(shù)能夠?qū)⒂抑缔D(zhuǎn)發(fā)過去息裸,但是并不能夠轉(zhuǎn)發(fā)左值访圃,解決辦法就是借助universal references通用引用類型和std::forward()模板函數(shù)共同實現(xiàn)完美轉(zhuǎn)發(fā)批糟。例子如下:
#include<iostream>#include<cstring>#include<vector>usingnamespacestd;voidRunCode(int&&m){cout<<"rvalue ref"<<endl;}voidRunCode(int&m){cout<<"lvalue ref"<<endl;}voidRunCode(constint&&m){cout<<"const rvalue ref"<<endl;}voidRunCode(constint&m){cout<<"const lvalue ref"<<endl;}// 這里利用了universal references满败,如果寫T&,就不支持傳入右值,而寫T&&侠讯,既能支持左值溜嗜,又能支持右值template<typenameT>voidperfectForward(T&&t){RunCode(forward<T>(t));}template<typenameT>voidnotPerfectForward(T&&t){RunCode(t);}intmain(){inta=0;intb=0;constintc=0;constintd=0;notPerfectForward(a);// lvalue refnotPerfectForward(move(b));// lvalue refnotPerfectForward(c);// const lvalue refnotPerfectForward(move(d));// const lvalue refcout<<endl;perfectForward(a);// lvalue refperfectForward(move(b));// rvalue refperfectForward(c);// const lvalue refperfectForward(move(d));// const rvalue ref}
上面的代碼測試結(jié)果表明谷扣,在universal references和std::forward的合作下,能夠完美的轉(zhuǎn)發(fā)這4種類型。
emplace_back減少內(nèi)存拷貝和移動
我們之前使用vector一般都喜歡用push_back()秃臣,由上文可知容易發(fā)生無謂的拷貝,解決辦法是為自己的類增加移動拷貝和賦值函數(shù)序攘,但其實還有更簡單的辦法慌核!就是使用emplace_back()替換push_back()伪货,如下面的例子:
#include<iostream>#include<cstring>#include<vector>usingnamespacestd;classA{public:A(inti){//? ? ? ? cout << "A()" << endl;str=to_string(i);}~A(){}A(constA&other):str(other.str){cout<<"A&"<<endl;}public:string str;};intmain(){vector<A>vec;vec.reserve(10);for(inti=0;i<10;i++){vec.push_back(A(i));//調(diào)用了10次拷貝構(gòu)造函數(shù)//? ? ? ? vec.emplace_back(i);? //一次拷貝構(gòu)造函數(shù)都沒有調(diào)用過}for(inti=0;i<10;i++)cout<<vec[i].str<<endl;}
可以看到效果是明顯的,雖然沒有測試時間,但是確實可以減少拷貝什荣。emplace_back()可以直接通過構(gòu)造函數(shù)的參數(shù)構(gòu)造對象笔横,但前提是要有對應(yīng)的構(gòu)造函數(shù)。
對于map和set,可以使用emplace()廊移。基本上emplace_back()對應(yīng)push_bakc(),emplce()對應(yīng)insert()界阁。
移動語義對swap()函數(shù)的影響也很大技健,之前實現(xiàn)swap可能需要三次內(nèi)存拷貝昔逗,而有了移動語義后鉴扫,就可以實現(xiàn)高性能的交換函數(shù)了雏吭。
template<typenameT>voidswap(T&a,T&b){Ttmp(std::move(a));a=std::move(b);b=std::move(tmp);}
如果T是可移動的姥饰,那么整個操作會很高效费什,如果不可移動闻察,那么就和普通的交換函數(shù)是一樣的鲸阻,不會發(fā)生什么錯誤震贵,很安全吟秩。
總結(jié)
由兩種值類型硼补,左值和右值鲤竹。
有三種引用類型辛藻,左值引用、右值引用和通用引用。左值引用只能綁定左值粒褒,右值引用只能綁定右值抠艾,通用引用由初始化時綁定的值的類型確定桂塞。
左值和右值是獨(dú)立于他們的類型的官辽,右值引用可能是左值可能是右值蛹磺,如果這個右值引用已經(jīng)被命名了,他就是左值同仆。
引用折疊規(guī)則:所有的右值引用疊加到右值引用上仍然是一個右值引用萤捆,其他引用折疊都為左值引用。當(dāng)T&&為模板參數(shù)時俗批,輸入左值俗或,它將變成左值引用,輸入右值則變成具名的右值應(yīng)用岁忘。
移動語義可以減少無謂的內(nèi)存拷貝辛慰,要想實現(xiàn)移動語義,需要實現(xiàn)移動構(gòu)造函數(shù)和移動賦值函數(shù)干像。
std::move()將一個左值轉(zhuǎn)換成一個右值帅腌,強(qiáng)制使用移動拷貝和賦值函數(shù),這個函數(shù)本身并沒有對這個左值什么特殊操作麻汰。
std::forward()和universal references通用引用共同實現(xiàn)完美轉(zhuǎn)發(fā)速客。
用empalce_back()替換push_back()增加性能。
TODO
對模板類型自動推導(dǎo)還不太熟悉五鲫,繼續(xù)學(xué)習(xí)Effective Modern C++溺职。
std::move()和std::forward()好像實現(xiàn)的并不復(fù)雜,有機(jī)會弄明白實現(xiàn)原理。
參考
深入應(yīng)用C++11:代碼優(yōu)化與工程級應(yīng)用
作者:StormZhu
鏈接:http://www.reibang.com/p/d19fc8447eaa
來源:簡書
著作權(quán)歸作者所有浪耘。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)智亮,非商業(yè)轉(zhuǎn)載請注明出處。