左值引用
int i = 0;
int& j = i;
這里的int&是對(duì)左值進(jìn)行綁定(但是int&卻不能綁定右值)狐赡,相應(yīng)的唯竹,對(duì)右值進(jìn)行綁定的引用就是右值引用沸停,他的語法是這樣的 A&&辆影,通過雙引號(hào)來表示綁定類型為A的右值良狈。通過&&我們就可以很方便的綁定右值了后添,比如我們可以這樣綁定一個(gè)右值:
int&& i = 0;
這里我們綁定了一個(gè)右值0,關(guān)于右值的概念會(huì)在后面介紹们颜。右值引用是C++11中新增加的一個(gè)很重要的特性吕朵,他主是要用來解決C++98/03中遇到的兩個(gè)問題:
1:臨時(shí)對(duì)象非必要的昂貴的拷貝操作,
2:是在模板函數(shù)中如何按照參數(shù)的實(shí)際類型進(jìn)行轉(zhuǎn)發(fā)窥突。
通過引入右值引用努溃,很好的解決了這兩個(gè)問題,改進(jìn)了程序性能阻问,后面將會(huì)詳細(xì)介紹右值引用是如何解決這兩個(gè)問題的梧税。
和右值引用相關(guān)的概念比較多,比如:右值、純右值第队、將亡值哮塞、universal references、引用折疊凳谦、移動(dòng)語義忆畅、move語義和完美轉(zhuǎn)發(fā)等等。
int i = getVar();
上面的這行代碼很簡(jiǎn)單尸执,從getVar()函數(shù)獲取一個(gè)整形值家凯,然而,這行代碼會(huì)產(chǎn)生幾種類型的值呢如失?
答案是會(huì)產(chǎn)生兩種類型的值绊诲,一種是左值i,一種是函數(shù)getVar()返回的臨時(shí)值褪贵,這個(gè)臨時(shí)值在表達(dá)式結(jié)束后就銷毀了掂之,而左值i在表達(dá)式結(jié)束后仍然存在,這個(gè)臨時(shí)值就是右值脆丁,具體來說是一個(gè)純右值世舰,右值是不具名的。區(qū)分左值和右值的一個(gè)簡(jiǎn)單辦法是:看能不能對(duì)表達(dá)式取地址偎快,如果能冯乘,則為左值,否則為右值晒夹。
所有的具名變量或?qū)ο蠖际亲笾雕陕涿兞縿t是右值,比如丐怯,簡(jiǎn)單的賦值語句:
int i = 0;
在這條語句中喷好,i 是左值,0 是字面量读跷,就是右值梗搅。在上面的代碼中,i 可以被引用效览,0 就不可以了无切。具體來說上面的表達(dá)式中等號(hào)右邊的0是純右值(prvalue)。
什么是右值
在C++11中所有的值必屬于左值丐枉、將亡值哆键、純右值三者之一。
比如瘦锹,非引用返回的臨時(shí)變量籍嘹、運(yùn)算表達(dá)式產(chǎn)生的臨時(shí)變量闪盔、原始字面量和lambda表達(dá)式等都是純右值。而將亡值是C++11新增的辱士、與右值引用相關(guān)的表達(dá)式泪掀,比如,將要被移動(dòng)的對(duì)象颂碘、T&&函數(shù)返回值异赫、std::move返回值和轉(zhuǎn)換為T&&的類型的轉(zhuǎn)換函數(shù)的返回值等。關(guān)于將亡值我們會(huì)在后面介紹凭涂,先看下面的代碼:
int j = 5;
auto f = []{return 5;};
上面的代碼中5是一個(gè)原始字面量祝辣, []{return 5;}是一個(gè)lambda表達(dá)式贴妻,都是屬于純右值切油,他們的特點(diǎn)是在表達(dá)式結(jié)束之后就銷毀了。
到此為止我們對(duì)右值有了一個(gè)初步的認(rèn)識(shí)名惩,知道了什么是右值澎胡。
右值引用介紹
右值引用的第一個(gè)特點(diǎn)
T&& k = getVar();
第二行代碼和第一行代碼很像,只是相比第一行代碼多了“&&”娩鹉,他就是右值引用攻谁。
我們知道左值引用是對(duì)左值的引用,那么弯予,對(duì)應(yīng)的戚宦,對(duì)右值的引用就是右值引用,而且右值是匿名變量锈嫩,我們也只能通過引用的方式來獲取右值受楼。
雖然這條代碼跟上面介紹的代碼起來差別不大,但是實(shí)際上語義的差別很大呼寸。
這里艳汽,getVar()產(chǎn)生的臨時(shí)值不會(huì)像第一行代碼那樣,在表達(dá)式結(jié)束之后就銷毀了对雪,而是會(huì)被“續(xù)命”河狐,他的生命周期將會(huì)通過右值引用得以延續(xù),和變量k的生命周期一樣長(zhǎng)瑟捣。
codedemo1-1
#include <iostream>
using namespace std;
int g_constructCount=0;
int g_copyConstructCount=0;
int g_destructCount=0;
struct A
{
A(){
cout<<"construct: "<<++g_constructCount<<endl;
}
A(const A& a)
{
cout<<"copy construct: "<<++g_copyConstructCount <<endl;
}
~A()
{
cout<<"destruct: "<<++g_destructCount<<endl;
}
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
為了清楚的觀察臨時(shí)值馋艺,在編譯時(shí)設(shè)置編譯選項(xiàng)-fno-elide-constructors用來關(guān)閉返回值優(yōu)化效果。
結(jié)果如下:
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o test_left test_left.cpp -fno-elide-constructors
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test_left
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr#
從上面的例子中可以看到迈套,在沒有返回值優(yōu)化的情況下捐祠,拷貝構(gòu)造函數(shù)調(diào)用了兩次,一次是GetA()函數(shù)內(nèi)部創(chuàng)建的對(duì)象返回出來構(gòu)造一個(gè)臨時(shí)對(duì)象產(chǎn)生的交汤,另一次是在main函數(shù)中構(gòu)造a對(duì)象產(chǎn)生的雏赦。第二次的destruct是因?yàn)榕R時(shí)對(duì)象在構(gòu)造a對(duì)象之后就銷毀了劫笙。如果開啟返回值優(yōu)化的話,輸出結(jié)果將是:
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o test_left test_left.cpp
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test_left
construct: 1
destruct: 1
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr#
可以看到返回值優(yōu)化將會(huì)把臨時(shí)對(duì)象優(yōu)化掉星岗,但這不是c++標(biāo)準(zhǔn)填大,是各編譯器的優(yōu)化規(guī)則。我們?cè)诨氐街疤岬降目梢酝ㄟ^右值引用來延長(zhǎng)臨時(shí)右值的生命周期俏橘,如果上面的代碼中我們通過右值引用來綁定函數(shù)返回值的話允华,結(jié)果又會(huì)是什么樣的呢?在編譯時(shí)設(shè)置編譯選項(xiàng)-fno-elide-constructors寥掐。
int main() {
A&& a = GetA();
return 0;
}
結(jié)果:
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test_left
construct: 1
copy construct: 1
destruct: 1
destruct: 2
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr#
通過右值引用靴寂,比之前少了一次拷貝構(gòu)造和一次析構(gòu),原因在于右值引用綁定了右值召耘,讓臨時(shí)右值的生命周期延長(zhǎng)了百炬。我們可以利用這個(gè)特點(diǎn)做一些性能優(yōu)化,即避免臨時(shí)對(duì)象的拷貝構(gòu)造和析構(gòu)污它,事實(shí)上剖踊,在c++98/03中,通過常量左值引用也經(jīng)常用來做性能優(yōu)化衫贬。上面的代碼改成:
const A& a = GetA();
輸出的結(jié)果和右值引用一樣德澈,因?yàn)槌A孔笾狄檬且粋€(gè)“萬能”的引用類型,可以接受左值固惯、右值梆造、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值葬毫,比如這樣的寫法是不對(duì)的:
A& a = GetA();
上面的代碼會(huì)報(bào)一個(gè)編譯錯(cuò)誤镇辉,因?yàn)榉浅A孔笾狄弥荒芙邮茏笾?/p>
右值引用的第二個(gè)特點(diǎn)
右值引用獨(dú)立于左值和右值。意思是右值引用類型的變量可能是左值也可能是右值供常。比如下面的例子:
int&& var1 = 1;
var1類型為右值引用摊聋,但var1本身是左值,因?yàn)榫呙兞慷际亲笾怠?/p>
關(guān)于右值引用一個(gè)有意思的問題是:T&&是什么栈暇,一定是右值嗎麻裁?讓我們來看看下面的例子:
template<typename T>
void f(T&& t){}
f(10);
int x = 10;
f(x);
從上面的代碼中可以看到,T&&表示的值類型不確定源祈,可能是左值又可能是右值煎源,這一點(diǎn)看起來有點(diǎn)奇怪,這就是右值引用的一個(gè)特點(diǎn)香缺。
右值引用的第三個(gè)特點(diǎn)
T&& t在發(fā)生自動(dòng)類型推斷的時(shí)候手销,它是未定的引用類型(universal references),如果被一個(gè)左值初始化图张,它就是一個(gè)左值锋拖;如果它被一個(gè)右值初始化诈悍,它就是一個(gè)右值,它是左值還是右值取決于它的初始化兽埃。
我們?cè)倩剡^頭看上面的代碼侥钳,對(duì)于函數(shù)template<typename T>void f(T&& t),當(dāng)參數(shù)為右值10的時(shí)候柄错,根據(jù)universal references的特點(diǎn)舷夺,t被一個(gè)右值初始化,那么t就是右值售貌;當(dāng)參數(shù)為左值x時(shí)给猾,t被一個(gè)左值引用初始化,那么t就是一個(gè)左值颂跨。需要注意的是敢伸,僅僅是當(dāng)發(fā)生自動(dòng)類型推導(dǎo)(如函數(shù)模板的類型自動(dòng)推導(dǎo),或auto關(guān)鍵字)的時(shí)候毫捣,T&&才是universal references详拙。再看看下面的例子:
template<typename T>
void f(T&& param);
template<typename T>
class Test {
Test(Test&& rhs);
};
上面的例子中,param是universal reference蔓同,rhs是Test&&右值引用,因?yàn)槟0婧瘮?shù)f發(fā)生了類型推斷蹲诀,而Test&&并沒有發(fā)生類型推導(dǎo)斑粱,因?yàn)門est&&是確定的類型了。
正是因?yàn)橛抑狄每赡苁亲笾狄部赡苁怯抑蹈Γ蕾囉诔跏蓟虮保⒉皇且幌伦泳痛_定的特點(diǎn),我們可以利用這一點(diǎn)做很多文章痕慢,比如后面要介紹的移動(dòng)語義和完美轉(zhuǎn)發(fā)尚揣。
這里再提一下引用折疊,正是因?yàn)橐肓擞抑狄靡淳伲钥赡艽嬖谧笾狄门c右值引用和右值引用與右值引用的折疊快骗,C++11確定了引用折疊的規(guī)則,規(guī)則是這樣的:
-所有的右值引用疊加到右值引用上仍然還是一個(gè)右值引用塔次;
-所有的其他引用類型之間的疊加都將變成左值引用方篮。
右值引用的作用
demo1-2
T(T&& a) : m_val(val){ a.m_val=nullptr; }
這行代碼實(shí)際上來自于一個(gè)類的構(gòu)造函數(shù),構(gòu)造函數(shù)的一個(gè)參數(shù)是一個(gè)右值引用励负,為什么將右值引用作為構(gòu)造函數(shù)的參數(shù)呢藕溅?在解答這個(gè)問題之前我們先看一個(gè)例子。如代碼清單1-2所示继榆。
#include <iostream>
using namespace std;
class A
{
public:
A():m_ptr(new int(0)){cout << "construct" << endl;}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷貝的拷貝構(gòu)造函數(shù)
{
cout << "copy construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
輸出結(jié)果
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o right_ref_test right_ref_test.cpp -fno-elide-constructors
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./right_ref_test construct
copy construct
copy construct
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr#
這個(gè)例子很簡(jiǎn)單巾表,一個(gè)帶有堆內(nèi)存的類汁掠,必須提供一個(gè)深拷貝拷貝構(gòu)造函數(shù),因?yàn)槟J(rèn)的拷貝構(gòu)造函數(shù)是淺拷貝集币,會(huì)發(fā)生“指針懸掛”的問題调塌。如果不提供深拷貝的拷貝構(gòu)造函數(shù),上面的測(cè)試代碼將會(huì)發(fā)生錯(cuò)誤(編譯選項(xiàng)-fno-elide-constructors)惠猿。
內(nèi)部的m_ptr將會(huì)被刪除兩次羔砾,一次是臨時(shí)右值析構(gòu)的時(shí)候刪除一次,第二次外面構(gòu)造的a對(duì)象釋放時(shí)刪除一次偶妖,而這兩個(gè)對(duì)象的m_ptr是同一個(gè)指針姜凄,這就是所謂的指針懸掛問題。提供深拷貝的拷貝構(gòu)造函數(shù)雖然可以保證正確趾访,但是在有些時(shí)候會(huì)造成額外的性能損耗态秧,因?yàn)橛袝r(shí)候這種深拷貝是不必要的。
上面代碼中的GetA函數(shù)會(huì)返回臨時(shí)變量扼鞋,然后通過這個(gè)臨時(shí)變量拷貝構(gòu)造了一個(gè)新的對(duì)象a申鱼,臨時(shí)變量在拷貝構(gòu)造完成之后就銷毀了,如果堆內(nèi)存很大的話云头,那么捐友,這個(gè)拷貝構(gòu)造的代價(jià)會(huì)很大,帶來了額外的性能損失溃槐。每次都會(huì)產(chǎn)生臨時(shí)變量并造成額外的性能損失匣砖,有沒有辦法避免臨時(shí)變量造成的性能損失呢?看看下面的代碼昏滴。
demo1-3
class A
{
public:
A() :m_ptr(new int(0)){}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷貝的拷貝構(gòu)造函數(shù)
{
cout << "copy construct" << endl;
}
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
輸出:
construct
move construct
move construct
這個(gè)構(gòu)造函數(shù)并沒有做深拷貝猴鲫,僅僅是將指針的所有者轉(zhuǎn)移到了另外一個(gè)對(duì)象,同時(shí)谣殊,將參數(shù)對(duì)象a的指針置為空拂共,這里僅僅是做了淺拷貝,因此姻几,這個(gè)構(gòu)造函數(shù)避免了臨時(shí)變量的深拷貝問題宜狐。
上面這個(gè)函數(shù)其實(shí)就是移動(dòng)構(gòu)造函數(shù),他的參數(shù)是一個(gè)右值引用類型鲜棠,這里的A&&表示右值肌厨,為什么?前面已經(jīng)提到豁陆,這里沒有發(fā)生類型推斷柑爸,是確定的右值引用類型。為什么會(huì)匹配到這個(gè)構(gòu)造函數(shù)盒音?因?yàn)檫@個(gè)構(gòu)造函數(shù)只能接受右值參數(shù)表鳍,而函數(shù)返回值是右值馅而,所以就會(huì)匹配到這個(gè)構(gòu)造函數(shù)。這里的A&&可以看作是臨時(shí)值的標(biāo)識(shí)譬圣,對(duì)于臨時(shí)值我們僅僅需要做淺拷貝即可瓮恭,無需再做深拷貝,從而解決了前面提到的臨時(shí)變量拷貝構(gòu)造產(chǎn)生的性能損失的問題厘熟。這就是所謂的移動(dòng)語義屯蹦,右值引用的一個(gè)重要作用是用來支持移動(dòng)語義的。
需要注意的一個(gè)細(xì)節(jié)是绳姨,我們提供移動(dòng)構(gòu)造函數(shù)的同時(shí)也會(huì)提供一個(gè)拷貝構(gòu)造函數(shù)登澜,以防止移動(dòng)不成功的時(shí)候還能拷貝構(gòu)造,使我們的代碼更安全飘庄。
我們知道移動(dòng)語義是通過右值引用來匹配臨時(shí)值的脑蠕,那么,普通的左值是否也能借助移動(dòng)語義來優(yōu)化性能呢跪削,那該怎么做呢谴仙?事實(shí)上C++11為了解決這個(gè)問題,提供了std::move方法來將左值轉(zhuǎn)換為右值碾盐,從而方便應(yīng)用移動(dòng)語義晃跺。move是將對(duì)象資源的所有權(quán)從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,只是轉(zhuǎn)移廓旬,沒有內(nèi)存的拷貝哼审,這就是所謂的move語義。
{
std::list< std::string> tokens;
//省略初始化...
std::list< std::string> t = tokens; //這里存在拷貝
}
std::list< std::string> tokens;
std::list< std::string> t = std::move(tokens); //這里沒有拷
如果不用std::move孕豹,拷貝的代價(jià)很大,性能較低十气。使用move幾乎沒有任何代價(jià)励背,只是轉(zhuǎn)換了資源的所有權(quán)。他實(shí)際上將左值變成右值引用砸西,然后應(yīng)用移動(dòng)語義叶眉,調(diào)用移動(dòng)構(gòu)造函數(shù),就避免了拷貝芹枷,提高了程序性能衅疙。如果一個(gè)對(duì)象內(nèi)部有較大的對(duì)內(nèi)存或者動(dòng)態(tài)數(shù)組時(shí),很有必要寫move語義的拷貝構(gòu)造函數(shù)和賦值函數(shù)鸳慈,避免無謂的深拷貝饱溢,以提高性能。事實(shí)上走芋,C++11中所有的容器都實(shí)現(xiàn)了移動(dòng)語義绩郎,方便我們做性能優(yōu)化潘鲫。
template<typename T>
typename remove_reference<T>::type&& move(T&& value){
return static_cast<typename remove_reference<T>::type&&>(value);
}
type:是C++11新增的 類型成員 類型成員與靜態(tài)成員一樣,它們都屬于類而不屬于對(duì)象肋杖,訪問它時(shí)也與訪問靜態(tài)成員一樣用::訪問
它表達(dá)的意思是返回remove_reference類的type類型成員溉仑。而該類是一個(gè)模板類,所以在它前面要加typename關(guān)鍵字状植。
remove_reference類其實(shí)就是就是通過模板去除引用
template <typename T>
struct remove_reference{
typedef T type; //定義T的類型別名為type
};
template <typename T>
struct remove_reference<T&> //左值引用
{
typedef T type;
}
template <typename T>
struct remove_reference<T&&> //右值引用
{
typedef T type;
}
通過上面的代碼我們可以知道浊竟,經(jīng)過remove_reference處理后,T的引用被剔除了津畸。假設(shè)前面我們通過move的類型自動(dòng)推導(dǎo)得到T為int&&振定,那么再次經(jīng)過模板推導(dǎo)remove_reference的type成員,這樣就可以得出type的類型為int了洼畅。
經(jīng)過翻譯后的代碼
int && move(int&& && t){
return static_case<int&&>(t);
}
//或
int && move(int& && t){
return static_case<int&&>(t);
}
具體解釋可看:C++高階知識(shí) std::move
完美轉(zhuǎn)發(fā)
直接看看代碼:
demo1-4
#include <iostream>
template<typename T>
void print(T && t){
std::cout << "右值" << std::endl;
}
template<typename T>
void print(T & t){
std::cout << "左值" << std::endl;
}
template<typename T>
void testForward(T && v){
print(v);
print(std::forward<T>(v));
print(std::move(v));
}
int main(int argc, char * argv[])
{
testForward(1);
std::cout << "======================" << std::endl;
int x = 1;
testForward(x);
return 0;
}
運(yùn)行結(jié)果:
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./forward_test2
左值
右值
右值
======================
左值
左值
右值
原理
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
forward實(shí)現(xiàn)了兩個(gè)模板函數(shù)吩案,一個(gè)接收左值,另一個(gè)接收右值帝簇。在上面有代碼中:
typename std::remove_reference<T>::type
其含義就是獲得去掉引用的參數(shù)類型徘郭。所以上面的兩上模板函數(shù)中,第一個(gè)是左值引用模板函數(shù)丧肴,第二個(gè)是右值引用模板函數(shù)残揉。
緊接著std::forward模板函數(shù)對(duì)傳入的參數(shù)進(jìn)行強(qiáng)制類型轉(zhuǎn)換,轉(zhuǎn)換的目標(biāo)類型符合引用折疊規(guī)則芋浮,因此左值參數(shù)最終轉(zhuǎn)換后仍為左值抱环,右值參數(shù)最終轉(zhuǎn)成右值。