左值引用、右值引用、移動(dòng)語(yǔ)義、完美轉(zhuǎn)發(fā),你知道的不知道的都在這里
眾所周知C++11新增了右值引用,談?dòng)抑狄梦覀円部梢詳U(kuò)展一些相關(guān)概念:
- 左值
- 右值
- 純右值
- 將亡值
- 左值引用
- 右值引用
- 移動(dòng)語(yǔ)義
- 完美轉(zhuǎn)發(fā)
- 返回值優(yōu)化
程序喵下面會(huì)一一介紹:
左值、右值
概念1:
左值:可以放到等號(hào)左邊的東西叫左值。
右值:不可以放到等號(hào)左邊的東西就叫右值。
概念2:
左值:可以取地址并且有名字的東西就是左值。
右值:不能取地址的沒(méi)有名字的東西就是右值。
舉例:
- int a = b + c;
a是左值,有變量名,可以取地址,也可以放到等號(hào)左邊, 表達(dá)式b+c的返回值是右值,沒(méi)有名字且不能取地址,&(b+c)不能通過(guò)編譯,而且也不能放到等號(hào)左邊。
- int a = 4; // a是左值,4作為普通字面量是右值
左值一般有:
- 函數(shù)名和變量名
- 返回左值引用的函數(shù)調(diào)用
- 前置自增自減表達(dá)式++i、--i
- 由賦值表達(dá)式或賦值運(yùn)算符連接的表達(dá)式(a=b, a += b等)
- 解引用表達(dá)式*p
- 字符串字面值"abcd"
純右值、將亡值
純右值和將亡值都屬于右值。
純右值
運(yùn)算表達(dá)式產(chǎn)生的臨時(shí)變量、不和對(duì)象關(guān)聯(lián)的原始字面量、非引用返回的臨時(shí)變量、lambda表達(dá)式等都是純右值。
舉例:
- 除字符串字面值外的字面值
- 返回非引用類(lèi)型的函數(shù)調(diào)用
- 后置自增自減表達(dá)式i++、i--
- 算術(shù)表達(dá)式(a+b, a*b, a&&b, a==b等)
- 取地址表達(dá)式等(&a)
將亡值
將亡值是指C++11新增的和右值引用相關(guān)的表達(dá)式,通常指將要被移動(dòng)的對(duì)象、T&&函數(shù)的返回值、std::move函數(shù)的返回值、轉(zhuǎn)換為T(mén)&&類(lèi)型轉(zhuǎn)換函數(shù)的返回值,將亡值可以理解為即將要銷(xiāo)毀的值,通過(guò)“盜取”其它變量?jī)?nèi)存空間方式獲取的值,在確保其它變量不再被使用或者即將被銷(xiāo)毀時(shí),可以避免內(nèi)存空間的釋放和分配,延長(zhǎng)變量值的生命周期,常用來(lái)完成移動(dòng)構(gòu)造或者移動(dòng)賦值的特殊任務(wù)。
舉例:
- class A {
- xxx;
- };
- A a;
- auto c = std::move(a); // c是將亡值
- auto d = static_cast<A&&>(a); // d是將亡值
左值引用、右值引用
根據(jù)名字大概就可以猜到意思,左值引用就是對(duì)左值進(jìn)行引用的類(lèi)型,右值引用就是對(duì)右值進(jìn)行引用的類(lèi)型,他們都是引用,都是對(duì)象的一個(gè)別名,并不擁有所綁定對(duì)象的堆存,所以都必須立即初始化。
- type &name = exp; // 左值引用
- type &&name = exp; // 右值引用
左值引用
看代碼:
- int a = 5;
- int &b = a; // b是左值引用
- b = 4;
- int &c = 10; // error,10無(wú)法取地址,無(wú)法進(jìn)行引用
- const int &d = 10; // ok,因?yàn)槭浅R?,引用常量?shù)字,這個(gè)常量數(shù)字會(huì)存儲(chǔ)在內(nèi)存中,可以取地址
可以得出結(jié)論:對(duì)于左值引用,等號(hào)右邊的值必須可以取地址,如果不能取地址,則會(huì)編譯失敗,或者可以使用const引用形式,但這樣就只能通過(guò)引用來(lái)讀取輸出,不能修改數(shù)組,因?yàn)槭浅A恳谩?/p>
右值引用
如果使用右值引用,那表達(dá)式等號(hào)右邊的值需要時(shí)右值,可以使用std::move函數(shù)強(qiáng)制把左值轉(zhuǎn)換為右值。
- int a = 4;
- int &&b = a; // error, a是左值
- int &&c = std::move(a); // ok
移動(dòng)語(yǔ)義
談移動(dòng)語(yǔ)義前,我們首先需要了解深拷貝與淺拷貝的概念
深拷貝、淺拷貝
直接拿代碼舉例:
- class A {
- public:
- A(int size) : size_(size) {
- data_ = new int[size];
- }
- A(){}
- A(const A& a) {
- size_ = a.size_;
- data_ = a.data_;
- cout << "copy " << endl;
- }
- ~A() {
- delete[] data_;
- }
- int *data_;
- int size_;
- };
- int main() {
- A a(10);
- A b = a;
- cout << "b " << b.data_ << endl;
- cout << "a " << a.data_ << endl;
- return 0;
- }
上面代碼中,兩個(gè)輸出的是相同的地址,a和b的data_指針指向了同一塊內(nèi)存,這就是淺拷貝,只是數(shù)據(jù)的簡(jiǎn)單賦值,那再析構(gòu)時(shí)data_內(nèi)存會(huì)被釋放兩次,導(dǎo)致程序出問(wèn)題,這里正常會(huì)出現(xiàn)double free導(dǎo)致程序崩潰的,但是不知道為什么我自己測(cè)試程序卻沒(méi)有崩潰,能力有限,沒(méi)搞明白,無(wú)論怎樣,這樣的程序肯定是有隱患的,如何消除這種隱患呢,可以使用如下深拷貝:
- class A {
- public:
- A(int size) : size_(size) {
- data_ = new int[size];
- }
- A(){}
- A(const A& a) {
- size_ = a.size_;
- data_ = new int[size_];
- cout << "copy " << endl;
- }
- ~A() {
- delete[] data_;
- }
- int *data_;
- int size_;
- };
- int main() {
- A a(10);
- A b = a;
- cout << "b " << b.data_ << endl;
- cout << "a " << a.data_ << endl;
- return 0;
- }
深拷貝就是再拷貝對(duì)象時(shí),如果被拷貝對(duì)象內(nèi)部還有指針引用指向其它資源,自己需要重新開(kāi)辟一塊新內(nèi)存存儲(chǔ)資源,而不是簡(jiǎn)單的賦值。
聊完了深拷貝淺拷貝,可以聊聊移動(dòng)語(yǔ)義啦:
移動(dòng)語(yǔ)義,在程序喵看來(lái)可以理解為轉(zhuǎn)移所有權(quán),之前的拷貝是對(duì)于別人的資源,自己重新分配一塊內(nèi)存存儲(chǔ)復(fù)制過(guò)來(lái)的資源,而對(duì)于移動(dòng)語(yǔ)義,類(lèi)似于轉(zhuǎn)讓或者資源竊取的意思,對(duì)于那塊資源,轉(zhuǎn)為自己所擁有,別人不再擁有也不會(huì)再使用,通過(guò)C++11新增的移動(dòng)語(yǔ)義可以省去很多拷貝負(fù)擔(dān),怎么利用移動(dòng)語(yǔ)義呢,是通過(guò)移動(dòng)構(gòu)造函數(shù)。
- class A {
- public:
- A(int size) : size_(size) {
- data_ = new int[size];
- }
- A(){}
- A(const A& a) {
- size_ = a.size_;
- data_ = new int[size_];
- cout << "copy " << endl;
- }
- A(A&& a) {
- this->data_ = a.data_;
- a.data_ = nullptr;
- cout << "move " << endl;
- }
- ~A() {
- if (data_ != nullptr) {
- delete[] data_;
- }
- }
- int *data_;
- int size_;
- };
- int main() {
- A a(10);
- A b = a;
- A c = std::move(a); // 調(diào)用移動(dòng)構(gòu)造函數(shù)
- return 0;
- }
如果不使用std::move(),會(huì)有很大的拷貝代價(jià),使用移動(dòng)語(yǔ)義可以避免很多無(wú)用的拷貝,提供程序性能,C++所有的STL都實(shí)現(xiàn)了移動(dòng)語(yǔ)義,方便我們使用。例如:
- std::vector<string> vecs;
- ...
- std::vector<string> vecm = std::move(vecs); // 免去很多拷貝
注意:移動(dòng)語(yǔ)義僅針對(duì)于那些實(shí)現(xiàn)了移動(dòng)構(gòu)造函數(shù)的類(lèi)的對(duì)象,對(duì)于那種基本類(lèi)型int、float等沒(méi)有任何優(yōu)化作用,還是會(huì)拷貝,因?yàn)樗鼈儗?shí)現(xiàn)沒(méi)有對(duì)應(yīng)的移動(dòng)構(gòu)造函數(shù)。
完美轉(zhuǎn)發(fā)
完美轉(zhuǎn)發(fā)指可以寫(xiě)一個(gè)接受任意實(shí)參的函數(shù)模板,并轉(zhuǎn)發(fā)到其它函數(shù),目標(biāo)函數(shù)會(huì)收到與轉(zhuǎn)發(fā)函數(shù)完全相同的實(shí)參,轉(zhuǎn)發(fā)函數(shù)實(shí)參是左值那目標(biāo)函數(shù)實(shí)參也是左值,轉(zhuǎn)發(fā)函數(shù)實(shí)參是右值那目標(biāo)函數(shù)實(shí)參也是右值。那如何實(shí)現(xiàn)完美轉(zhuǎn)發(fā)呢,答案是使用std::forward()。
- void PrintV(int &t) {
- cout << "lvalue" << endl;
- }
- void PrintV(int &&t) {
- cout << "rvalue" << endl;
- }
- template<typename T>
- void Test(T &&t) {
- PrintV(t);
- PrintV(std::forward<T>(t));
- PrintV(std::move(t));
- }
- int main() {
- Test(1); // lvalue rvalue rvalue
- int a = 1;
- Test(a); // lvalue lvalue rvalue
- Test(std::forward<int>(a)); // lvalue rvalue rvalue
- Test(std::forward<int&>(a)); // lvalue lvalue rvalue
- Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
- return 0;
- }
分析
- Test(1):1是右值,模板中T &&t這種為萬(wàn)能引用,右值1傳到Test函數(shù)中變成了右值引用,但是調(diào)用PrintV()時(shí)候,t變成了左值,因?yàn)樗兂闪艘粋€(gè)擁有名字的變量,所以打印lvalue,而PrintV(std::forward<T>(t))時(shí)候,會(huì)進(jìn)行完美轉(zhuǎn)發(fā),按照原來(lái)的類(lèi)型轉(zhuǎn)發(fā),所以打印rvalue,PrintV(std::move(t))毫無(wú)疑問(wèn)會(huì)打印rvalue。
- Test(a):a是左值,模板中T &&這種為萬(wàn)能引用,左值a傳到Test函數(shù)中變成了左值引用,所以有代碼中打印。
- Test(std::forward<T>(a)):轉(zhuǎn)發(fā)為左值還是右值,依賴(lài)于T,T是左值那就轉(zhuǎn)發(fā)為左值,T是右值那就轉(zhuǎn)發(fā)為右值。
返回值優(yōu)化
返回值優(yōu)化(RVO)是一種C++編譯優(yōu)化技術(shù),當(dāng)函數(shù)需要返回一個(gè)對(duì)象實(shí)例時(shí)候,就會(huì)創(chuàng)建一個(gè)臨時(shí)對(duì)象并通過(guò)復(fù)制構(gòu)造函數(shù)將目標(biāo)對(duì)象復(fù)制到臨時(shí)對(duì)象,這里有復(fù)制構(gòu)造函數(shù)和析構(gòu)函數(shù)會(huì)被多余的調(diào)用到,有代價(jià),而通過(guò)返回值優(yōu)化,C++標(biāo)準(zhǔn)允許省略調(diào)用這些復(fù)制構(gòu)造函數(shù)。
那什么時(shí)候編譯器會(huì)進(jìn)行返回值優(yōu)化呢?
- return的值類(lèi)型與函數(shù)的返回值類(lèi)型相同
- return的是一個(gè)局部對(duì)象
看幾個(gè)例子:
示例1:
- std::vector<int> return_vector(void) {
- std::vector<int> tmp {1,2,3,4,5};
- return tmp;
- }
- std::vector<int> &&rval_ref = return_vector();
不會(huì)觸發(fā)RVO,拷貝構(gòu)造了一個(gè)臨時(shí)的對(duì)象,臨時(shí)對(duì)象的生命周期和rval_ref綁定,等價(jià)于下面這段代碼:
- const std::vector<int>& rval_ref = return_vector();
示例2:
- std::vector<int>&& return_vector(void) {
- std::vector<int> tmp {1,2,3,4,5};
- return std::move(tmp);
- }
- std::vector<int> &&rval_ref = return_vector();
這段代碼會(huì)造成運(yùn)行時(shí)錯(cuò)誤,因?yàn)閞val_ref引用了被析構(gòu)的tmp。講道理來(lái)說(shuō)這段代碼是錯(cuò)的,但我自己運(yùn)行過(guò)程中卻成功了,我沒(méi)有那么幸運(yùn),這里不糾結(jié),繼續(xù)向下看什么時(shí)候會(huì)觸發(fā)RVO。
示例3:
- std::vector<int> return_vector(void) {
- std::vector<int> tmp {1,2,3,4,5};
- return std::move(tmp);
- }
- std::vector<int> &&rval_ref = return_vector();
和示例1類(lèi)似,std::move一個(gè)臨時(shí)對(duì)象是沒(méi)有必要的,也會(huì)忽略掉返回值優(yōu)化。
最好的代碼:
- std::vector<int> return_vector(void) {
- std::vector<int> tmp {1,2,3,4,5};
- return tmp;
- }
- std::vector<int> rval_ref = return_vector();
這段代碼會(huì)觸發(fā)RVO,不拷貝也不移動(dòng),不生成臨時(shí)對(duì)象。