淺析C++11中的右值引用、轉(zhuǎn)移語(yǔ)義和完美轉(zhuǎn)發(fā)
1. 左值與右值:
C++對(duì)于左值和右值沒(méi)有標(biāo)準(zhǔn)定義,但是有一個(gè)被廣泛認(rèn)同的說(shuō)法:可以取地址的,有名字的,非臨時(shí)的就是左值;不能取地址的,沒(méi)有名字的,臨時(shí)的就是右值.
可見(jiàn)立即數(shù),函數(shù)返回的值等都是右值;而非匿名對(duì)象(包括變量),函數(shù)返回的引用,const對(duì)象等都是左值.
從本質(zhì)上理解,創(chuàng)建和銷(xiāo)毀由編譯器幕后控制的,程序員只能確保在本行代碼有效的,就是右值(包括立即數(shù));而用戶(hù)創(chuàng)建的,通過(guò)作用域規(guī)則可知其生存期的,就是左值(包括函數(shù)返回的局部變量的引用以及const對(duì)象),例如:
int& foo(){int tmp; return tmp;}
int fooo(){int tmp; return tmp;}
int a=10;
const int b;
int& temp=foo();//雖然合法,但temp引用了一個(gè)已經(jīng)不存在的對(duì)象
int tempp=fooo();
以上代碼中,a,temp和foo()都是非常量左值,b是常量左值,fooo()是非常量右值,10是常量右值,有一點(diǎn)要特別注意:返回的引用是左值(可以取地址)!
一般來(lái)說(shuō),編譯器是不允許對(duì)右值進(jìn)行更改的(因?yàn)橛抑档纳嫫诓挥沙绦騿T掌握,即使更改了右值也未必可以用),對(duì)于內(nèi)置類(lèi)型對(duì)象尤其如此,但C++允許使用右值對(duì)象調(diào)用成員函數(shù),雖然允許這樣做,但出于同樣原因,最好不要這么做.
2. 右值引用:
右值引用的表示方法為
Datatype&& variable
右值引用是C++ 11新增的特性,所以C++ 98的引用為左值引用.右值引用用來(lái)綁定到右值,綁定到右值以后本來(lái)會(huì)被銷(xiāo)毀的右值的生存期會(huì)延長(zhǎng)至與綁定到它的右值引用的生存期,右值引用的存在并不是為了取代左值引用,而是充分利用右值(特別是臨時(shí)對(duì)象)的建構(gòu)來(lái)減少對(duì)象建構(gòu)和析構(gòu)操作以達(dá)到提高效率的目的,例如對(duì)于以下函數(shù):
(Demo是一個(gè)類(lèi))
Demo foo(){
Demo tmp;
return tmp;
}
在編譯器不進(jìn)行RVO(return value optimization)優(yōu)化的前提下以下操作:
Demo x=foo();
將會(huì)調(diào)用三次構(gòu)造函數(shù)(tmp的,x的,臨時(shí)對(duì)象的),相應(yīng)的在對(duì)象被銷(xiāo)毀時(shí)也會(huì)調(diào)用三次析構(gòu)函數(shù),而如果采用右值引用的方式:
Demo&& x=foo();
那么就不需要進(jìn)行x的建構(gòu),本來(lái)本來(lái)要被銷(xiāo)毀的臨時(shí)對(duì)象也會(huì)由于x的綁定而將生存期延長(zhǎng)至和x一樣(可以理解為x賦予了那個(gè)臨時(shí)對(duì)象一個(gè)合法地位:一個(gè)名字),就需要提高了效率(代價(jià)就是tmp需要占據(jù)4字節(jié)空間,但這是微不足道的).
右值引用與左值引用綁定規(guī)則:
常量左值引用可以綁定到常量和非常量左值,常量和非常量右值;
非常量左值引用只能綁定到非常量左值;
非常量右值引用只能綁定到非常量右值(vs2013也可以綁定到常量右值);
常量右值引用只能綁定到常量和非常量右值(非常量右值引用只是為了語(yǔ)義的完整而存在,常量左值引用就可以實(shí)現(xiàn)它的作用).
雖然從綁定規(guī)則中可以看出常量左值引用也可以綁定到右值,但顯然不可以改變右值的值,右值引用就可以,從而實(shí)現(xiàn)轉(zhuǎn)移語(yǔ)義,因?yàn)橛抑狄猛ǔR淖兯壎ǖ挠抑?所以被綁定的右值不能為const.
注意:右值引用是左值!
3. 轉(zhuǎn)移語(yǔ)義(move semantics):
右值引用被引入的目的之一就是實(shí)現(xiàn)轉(zhuǎn)移語(yǔ)義,轉(zhuǎn)移語(yǔ)義可以將資源 ( 堆,系統(tǒng)對(duì)象等 ) 的所有權(quán)從一個(gè)對(duì)象(通常是匿名的臨時(shí)對(duì)象)轉(zhuǎn)移到另一個(gè)對(duì)象,從而減少對(duì)象構(gòu)建及銷(xiāo)毀操作,提高程序效率(這在2的例子中已經(jīng)作了解釋).轉(zhuǎn)移語(yǔ)義與拷貝語(yǔ)義是相對(duì)的.從轉(zhuǎn)移語(yǔ)義可以看出,實(shí)際上,轉(zhuǎn)移語(yǔ)義并不是新的概念,它實(shí)際上已經(jīng)在C++98/03的語(yǔ)言和庫(kù)中被使用了,比如在某些情況下拷貝構(gòu)造函數(shù)的省略(copy constructor elision in some contexts),智能指針的拷貝(auto_ptr “copy”),鏈表拼接(list::splice)和容器內(nèi)的置換(swap on containers)等,只是還沒(méi)有統(tǒng)一的語(yǔ)法和語(yǔ)義支持
雖然普通的函數(shù)和操作符也可以利用右值引用實(shí)現(xiàn)轉(zhuǎn)移語(yǔ)義(如2中的例子),但轉(zhuǎn)移語(yǔ)義通常是通過(guò)轉(zhuǎn)移構(gòu)造函數(shù)和轉(zhuǎn)移賦值操作符實(shí)現(xiàn)的.轉(zhuǎn)移構(gòu)造函數(shù)的原型為Classname(Typename&&) ,而拷貝構(gòu)造函數(shù)的原型為Classname(const Typename&) ,轉(zhuǎn)移構(gòu)造函數(shù)不會(huì)被編譯器自動(dòng)生成,需要自己定義,只定義轉(zhuǎn)移構(gòu)造函數(shù)也不影響編譯器生成拷貝構(gòu)造函數(shù),如果傳遞的參數(shù)是左值,就調(diào)用拷貝構(gòu)造函數(shù),反之,就調(diào)用轉(zhuǎn)移構(gòu)造函數(shù).
例如:
class Demo{
public:
Demo():p(new int[10000]{};
Demo(Demo&& lre):arr(lre.arr),size(lra.size){lre.arr=NULL;}//轉(zhuǎn)移構(gòu)造函數(shù)
Demo(const Demo& lre):arr(new int[10000]),size(arr.size){
for(int cou=0;cou<10000;++cou)
arr[cou]=lew.arr[cou];
}
private:
int size;
int* arr;
}
從以上代碼可以看出,拷貝構(gòu)造函數(shù)在堆中重新開(kāi)辟了一個(gè)大小為10000的int型數(shù)組,然后每個(gè)元素分別拷貝,而轉(zhuǎn)移構(gòu)造函數(shù)則是直接接管參數(shù)的指針?biāo)赶虻馁Y源,效率搞下立判!需要注意的是轉(zhuǎn)移構(gòu)造函數(shù)實(shí)參必須是右值,一般是臨時(shí)對(duì)象,如函數(shù)的返回值等,對(duì)于此類(lèi)臨時(shí)對(duì)象一般在當(dāng)行代碼之后就被銷(xiāo)毀,而采用轉(zhuǎn)移構(gòu)造函數(shù)可以延長(zhǎng)其生命期,可謂是物盡其用,同時(shí)有避免了重新開(kāi)辟數(shù)組.對(duì)于上述代碼中的轉(zhuǎn)移構(gòu)造函數(shù),有必要詳細(xì)分析一下:
Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}
lre是一個(gè)右值引用,通過(guò)它間接訪問(wèn)實(shí)參(臨時(shí)對(duì)象)的資源來(lái)完成資源轉(zhuǎn)移,lre綁定的對(duì)象(必須)是右值,但lre本身是左值;
因?yàn)?code>lre是函數(shù)的局部對(duì)象,”lre.arr=NULL"必不可少,否則函數(shù)結(jié)尾調(diào)用析構(gòu)函數(shù)銷(xiāo)毀lre時(shí)仍然會(huì)將資源釋放,轉(zhuǎn)移的資源還是被系統(tǒng)收回.
4. move()函數(shù)
3中的例子并非萬(wàn)能,Demo(Demo&& lre)的實(shí)參必須是右值,有時(shí)候一個(gè)左值即將到達(dá)生存期,但是仍然想要使用轉(zhuǎn)移語(yǔ)義接管它的資源,這時(shí)就需要move函數(shù).
std::move函數(shù)定義在標(biāo)準(zhǔn)庫(kù)<utility>中,它的作用是將左值強(qiáng)行轉(zhuǎn)化為右值使用,從實(shí)現(xiàn)上講,std:move等同于static_cast<T&&>(lvalue) ,由此看出,被轉(zhuǎn)化的左值本身的生存期和左值屬性并沒(méi)有被改變,這類(lèi)似于const_cast函數(shù).因此被move的實(shí)參應(yīng)該是即將到達(dá)生存期的左值,否則的話(huà)可能起到反面效果.
5. 完美轉(zhuǎn)發(fā)(perfect forwarding)
完美轉(zhuǎn)發(fā)指的是將一組實(shí)參"完美"地傳遞給形參,完美指的是參數(shù)的const屬性與左右值屬性不變,例如在進(jìn)行函數(shù)包裝的時(shí)候,func函數(shù)存在下列重載:
void func(const int); void func(int); void func(int&&);
如果要將它們包裝到一個(gè)函數(shù)cover內(nèi),以實(shí)現(xiàn):
void cover(typename para){
func(para);
}
使得針對(duì)不同實(shí)參能在cover內(nèi)調(diào)用相應(yīng)類(lèi)型的函數(shù),似乎只能通過(guò)對(duì)cover進(jìn)行函數(shù)重載,這使代碼變得冗繁,另一種方法就是使用函數(shù)模板,但在C++ 11之前,實(shí)現(xiàn)該功能的函數(shù)模板只能采用值傳遞,如下:
template<typename T>
void cover(T para){
...
func(para);
...
}
但如果傳遞的是一個(gè)相當(dāng)大的對(duì)象,又會(huì)造成效率問(wèn)題,要通過(guò)引用傳遞實(shí)現(xiàn)形參與實(shí)參的完美匹配(包裹const屬性與左右值屬性的完美匹配),就要使用C++ 11 新引入的引用折疊規(guī)則:
函數(shù)形參 T的類(lèi)型 推導(dǎo)后的函數(shù)形參
T& A& A&
T& A&& A&
T&& A& A&
T&& A&& A&&
因此,對(duì)于前例的函數(shù)包裝要求,采用以下模板就可以解決:
template<typename T>
void cover(T&& para){
...
func(static_cast<T &&>(para));
...
}
如果傳入的是左值引用,轉(zhuǎn)發(fā)函數(shù)將被實(shí)例化為:
void func(T& && para){
func(static_cast<T& &&>(para));
}
應(yīng)用引用折疊,就為:
void func(T& para){
func(static_cast<T&>(para));
}
如果傳入的是右值引用,轉(zhuǎn)發(fā)函數(shù)將被實(shí)例化為:
void func(T&& &¶){
func(static_cast<T&& &&>(para));
}
應(yīng)用引用折疊,就是:
void func(T&& para){
func(static_cast<T&&>(para));
}
對(duì)于以上的static_cast<T&&> ,實(shí)際上只在para被推導(dǎo)為右值引用的時(shí)候才發(fā)揮作用,由于para是左值(右值引用是左值),因此需要將它轉(zhuǎn)為右值后再傳入func內(nèi),C++ 11在<untility>定義了一個(gè)std::forward<T>函數(shù)來(lái)實(shí)現(xiàn)以上行為,
所以最終版本為
template<typename T>
void cover(T&& para){
func(forward(forward<T>(para)));
}
std::forward的實(shí)現(xiàn)與static_cast<T&&>(para)稍有不同
std::forward函數(shù)的用法為forward<T>(para) , 若T為左值引用,para將被轉(zhuǎn)換為T類(lèi)型的左值,否則para將被轉(zhuǎn)換為T類(lèi)型右值
總結(jié)
以上就是關(guān)于C++11中右值引用、轉(zhuǎn)移語(yǔ)義和完美轉(zhuǎn)發(fā)的全部?jī)?nèi)容,這篇文章介紹的很詳細(xì),希望對(duì)大家的學(xué)習(xí)工作能有所幫助。
相關(guān)文章
C++代碼和可執(zhí)行程序在x86和arm上的區(qū)別介紹
這篇文章主要介紹了C++代碼和可執(zhí)行程序在x86和arm上的區(qū)別,X86和ARM是占據(jù)CPU市場(chǎng)的兩大處理器,各有優(yōu)劣,本文給大家詳細(xì)介紹了兩者的區(qū)別,需要的朋友可以參考下2022-07-07
C++實(shí)現(xiàn)LeetCode( 69.求平方根)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode( 69.求平方根),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
C++?測(cè)試框架GoogleTest入門(mén)介紹
這篇文章主要為大家介紹了C++測(cè)試框架GoogleTest入門(mén)基礎(chǔ),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
詳解C語(yǔ)言?xún)?nèi)核中的自旋鎖結(jié)構(gòu)
自旋鎖是內(nèi)核中提供的一種高IRQL鎖,用同步以及獨(dú)占的方式訪問(wèn)某個(gè)資源。自旋鎖是為了解決內(nèi)核鏈表讀寫(xiě)時(shí)存在線(xiàn)程同步問(wèn)題。本文將講解一下自旋鎖的簡(jiǎn)單應(yīng)用,感興趣的可以了解一下2022-09-09
VSCode 配置C++開(kāi)發(fā)環(huán)境的方法步驟
這篇文章主要介紹了VSCode 配置C++開(kāi)發(fā)環(huán)境的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
C語(yǔ)言實(shí)現(xiàn) 數(shù)據(jù)類(lèi)型占多少字節(jié)指針占多少字節(jié)
這篇文章主要介紹了 C語(yǔ)言 數(shù)據(jù)類(lèi)型占多少字節(jié)指針占多少字節(jié)的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09
詳解C語(yǔ)言中return返回函數(shù)局部變量的問(wèn)題
本文主要介紹了C語(yǔ)言中return返回函數(shù)局部變量的問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
你知道C語(yǔ)言函數(shù)調(diào)用常用的2種方式嗎
本篇博客會(huì)講解C語(yǔ)言函數(shù)調(diào)用的2種方式,分別是:傳值調(diào)用和傳址調(diào)用。這2種函數(shù)調(diào)用方式有什么區(qū)別呢?為什么會(huì)有不同的效果呢?分別有哪些用途呢?下面就來(lái)一一展開(kāi)2023-04-04

