如果你還不知道C++11引入的右值、移動語義、完美轉(zhuǎn)發(fā)是什么,可以閱讀這篇文章;如果你已經(jīng)對這些知識了如指掌,也可以看看有什么可以補充~😏
創(chuàng)新互聯(lián)公司2013年成立,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項目成都網(wǎng)站制作、網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)網(wǎng)站策劃,項目實施與項目整合能力。我們以讓每一個夢想脫穎而出為使命,1280元新樂做網(wǎng)站,已為上家服務(wù),為新樂各地企業(yè)和個人服務(wù),聯(lián)系電話:18982081108一、右值 值類別vs變量類型在正式認識右值之前,我們要先區(qū)分值的類別和變量類型:
值類別可以被劃分左值和右值。
那什么是左值和右值呢?左值是能被取地址、不能被移動的值。右值是表達式中間結(jié)果/函數(shù)返回值(可能擁有變量名,也可能沒有)。
有一個可以區(qū)分左值和右值的便捷方法:看能不能對表達式取地址,如果能,則為左值,否則為右值。
C++11擴展了右值的概念,將右值分為了純右值和將亡值,但本文不作討論。
如下的示例將幫助我們區(qū)分左值和右值:
int i = 3; // i是左值,3是右值
int j = i+8; // j是左值,i+8是右值
char a = getCh(); // a是左值 ,getCh()的返回值是右值(臨時變量)
左值引用、右值引用、常引用在以前的文章中,我們曾經(jīng)討論過左值引用和常引用的區(qū)別。在本篇文章中,我們需要進一步系統(tǒng)的了解它們?nèi)咧g的關(guān)系。
引用類型 可以分為兩種:
&
符號引用左值(但不能引用右值),&&
符號引用右值(可以移動左值)。在C++11中,因為增加了右值引用(rvalue reference)的概念,所以C++98中的引用都稱為了左值引用(lvalue reference)。
使用方法如下所示:
int&& a = 3; // 3是右值,a是右值引用
int b = 8; // b是左值
int& bb = b; //bb是左值引用
int&& c = b + 5; // b+5是右值,c是右值引用
AA&& aa = getTemp(); // getTemp()的返回值是右值(臨時變量)
左值引用十分常見,我們知道是給變量取個別名,但是引入右值引用的意義是什么呢?(將在下文中解答)
在上述的代碼中,getTemp()的返回值本來在表達式語句結(jié)束后,其生命也就該終結(jié)了(因為是臨時變量),而通過右值引用重獲了新生,其生命周期將與右值引用類型變量aa的生命周期一樣,只要aa還活著,該右值臨時變量將會一直存活下去。
在下面的代碼中將幫助我們區(qū)分左值引用和右值引用:
void func(T& a);//1,參數(shù)是左值
void func(T&& a);//2,參數(shù)是右值
//T類型的變量
T var;
T& rvar1 = var;//正確,rvar1是左值
T& rvar1 = T{};//錯誤,左值引用不能引用右值
T&& rvar2 = T{};//正確,rvar2是右值
T&& rvar2 = var;//錯誤,右值引用不能引用左值
T&& rvar2 = std::move(var);//正確,可以通過std::move()將左值轉(zhuǎn)為右值引用
func(var);//進入1,a是左值
func(T{});//進入2,a是右值
func(rvar1);//進入1,a是左值
func(rvar2);//進入1,rvar2是右值引用但a是左值
可以看出:
rvar1
在初始化時,不能綁定右值T{}
,rvar2
在初始化時,不能綁定左值var
,但是可以通過std::move()
將左值轉(zhuǎn)為右值引用。rvar2
作為實參傳入func
中時,在作用域內(nèi)是左值(已命名的右值引用是左值)。另外,C++還支持了常引用,能夠同時接受左值和右值(作為常引用)。
void func(const T& a);//a是常引用
常引用和右值引用 都能接受右值的綁定,有什么區(qū)別呢?
現(xiàn)在回到我們的問題:引入右值引用的意義是什么?
如果函數(shù)重載能夠同時接受:右值引用/常引用參數(shù),則編譯器將優(yōu)先重載:右值引用參數(shù),即引入右值引用的主要目的是實現(xiàn)移動語義。
下面是不同值作為實參傳入形參時,函數(shù)重載優(yōu)先級(數(shù)字越小優(yōu)先級越高):
實參/形參 | T& | const T& | T&& | const T&& |
---|---|---|---|---|
左值 | 1 | 2 | ||
常左值 | 1 | |||
右值 | 3 | 1 | 2 | |
常右值 | 2 | 1 |
在正式學習移動語義(move semantic)和完美轉(zhuǎn)發(fā)std::forward()之前,我們還要提一嘴引用折疊(reference collapsing),它是移動語義和完美轉(zhuǎn)發(fā)的實現(xiàn)基礎(chǔ)。
using Lref = Data&;
using Rref = Data&&;
Data data;
Lref& r1 = data; // r1是左值
Lref&& r2 = data; // r2是左值
Rref& r3 = data; // r3是左值
Rref&& r4 = Data{}; // r4是右值
總之,只有右值引用折疊到右值引用上仍然是一個右值引用,而其他所有的引用類型之間的折疊都將變成左值引用。
二、移動語義 為什么需要移動語義我們知道,如果一個對象中有堆區(qū)資源,需要編寫拷貝構(gòu)造函數(shù)和賦值函數(shù),實現(xiàn)深拷貝。如果被拷貝的對象是臨時的,拷貝完就沒什么用了,這樣會造成沒有意義的資源申請和釋放操作。如果能將對象包含的資源,直接從舊對象移動到新對象,就可以節(jié)省資源申請和釋放的時間。C++11新增加的移動語義(move semantic)就是為了做到這一點(基本類型不包含資源,其移動和拷貝相同。)。
還有另一種情況:如果資源對象本身不可拷貝(如智能指針std::unique_ptr)需要定義移動構(gòu)造/移動賦值函數(shù),其原理類似。
實現(xiàn)移動語義要增加兩個函數(shù):移動構(gòu)造函數(shù)和移動賦值函數(shù)。我們通過實現(xiàn)一個簡單的string類對象來說明:
class String
{public:
String()
{cout<< "String類"<< this<< "的構(gòu)造函數(shù)"<< endl;// 顯示自己被調(diào)用的日志
const char* s = "Hello C++";
int len = strlen(s);
_str = new char[len + 1];
strcpy(_str, s);
}
String(const String& another)
{cout<< "String類"<< this<< "的拷貝構(gòu)造"<< endl;// 顯示自己被調(diào)用的日志
int len = strlen(another._str);//獲取源對象中字符串的長度
_str = new char[len + 1];
strcpy(_str, another._str);// 把數(shù)據(jù)從源對象中拷貝過來
}
String& operator=(String& another)
{cout<< "String類"<< this<< "的拷貝賦值"<< endl;// 顯示自己被調(diào)用的日志
if (this == &another)
return *this;// 避免自我賦值
int len = strlen(another._str);//獲取源對象中字符串的長度
_str = new char[len + 1];
strcpy(_str, another._str);// 把數(shù)據(jù)從源對象中拷貝過來
return *this;
}
String(String&& another)noexcept
{cout<< "String類"<< this<< "的移動構(gòu)造"<< endl;// 顯示自己被調(diào)用的日志
if(_str != nullptr)
delete[] _str; //如果已分配內(nèi)存,先釋放掉
this->_str = another._str;// 把資源從源對象中轉(zhuǎn)移過來
another._str = nullptr;// 把源對象中的指針置空
}
String& operator=(String&& another)
{cout<< "String類"<< this<< "的移動賦值"<< endl;// 顯示自己被調(diào)用的日志
if (this == &another)
return *this;// 避免自我賦值
if(_str != nullptr)
delete[] _str; //如果已分配內(nèi)存,先釋放掉
this->_str = another._str;// 把資源從源對象中轉(zhuǎn)移過來
another._str = nullptr;// 把源對象中的指針置空
return *this;
}
friend ostream& operator<<(ostream& out, String& str)
{out<< str._str;
return out;
}
~String()
{cout<< "String類"<< this<< "的析構(gòu)函數(shù)"<< endl;// 顯示自己被調(diào)用的日志
if(_str != nullptr)
{delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
上述代碼中,編譯器會根據(jù)傳入的實參的優(yōu)先級(詳見上文表格),來決定重載的構(gòu)造函數(shù)。
測試用例及輸出的結(jié)果:
int main()
{{String str1{};//String類0x751f9ffd60的構(gòu)造函數(shù)
cout<< "str1 = "<< str1<< endl;//str1 = Hello C++
String str2{str1};//String類0x751f9ffd58的拷貝構(gòu)造
cout<< "str2 = "<< str2<< endl;//str2 = Hello C++
//返回一個右值(臨時對象)的lambda函數(shù)
auto f = [] {String aa; return aa;};
// String str3 = f();//拷貝省略
// cout<< "str3 = "<< str3<< endl;
String str{};//String類0x751f9ffd48的構(gòu)造函數(shù)
String str3{move(str)};//String類0x751f9ffd40的移動構(gòu)造
cout<< "str3 = "<< str3<< endl;//str3 = Hello C++
String str4{};//String類0x751f9ffd38的構(gòu)造函數(shù)
//String類0x751f9ffd68的構(gòu)造函數(shù)
str4 = f();//String類0x751f9ffd38的移動賦值
//String類0x751f9ffd68的析構(gòu)函數(shù)
cout<< "str4 = "<< str4<< endl;//str4 = Hello C++
}
//String類0x751f9ffd38的析構(gòu)函數(shù)
//String類0x751f9ffd40的析構(gòu)函數(shù)
//String類0x751f9ffd48的析構(gòu)函數(shù)
//String類0x751f9ffd58的析構(gòu)函數(shù)
//String類0x751f9ffd60的析構(gòu)函數(shù)
system("pause");
return 0;
}
盡管C++11引入了移動語義,但是仍有優(yōu)化的空間——與其調(diào)用一次沒有意義的移動構(gòu)造函數(shù),不如讓編譯器直接跳過這個過程——于是就有了拷貝省略(copy elision)。
移動語義和拷貝省略的區(qū)別:
由于拷貝省略的存在,在上述代碼中,String str3 = f();
會被編譯器優(yōu)化,為了方便演示移動構(gòu)造函數(shù),我們使用了std::move()
的方法移動返回值,當然這會造成不必要的開銷。
通用引用閱讀本節(jié)需要讀者有一定的模板編程基礎(chǔ)。
C++11中引入了變長模板的概念,允許向模板參數(shù)里傳入不同類型的不定長引用參數(shù)。由于每個類型可能是左值引用或右值引用,針對所有可能的左右值引用組合,特化所有模板 是不現(xiàn)實的。
如果沒用通用引用的概念,那么對于一個變長模板函數(shù),至少需要兩個重載:
templatevoid func(T& arg, Args&...args)
{func(args...);//左值直接展開
}
templatevoid func(T&& arg, Args&&...args)
{func(std::move(args...));//右值需要std::move()轉(zhuǎn)發(fā)
}
Scott Meyers(Effective Modern C++的作者)指出“有時候符號&&
并不一定代表右值引用,它也可能是左值引用?!?/p>
事實上,如果一個引用符號需要通過推導才能得出左右值的類型(如模板參數(shù)類型或者auto
),那么這個符號就可以是左值引用或右值引用——這就是通用引用 (universal reference)。
這一點我們通過上文中的引用折疊的示例也可以得出。
基于通用引用,我們可以對上述的代碼進行改進:
templatevoid func(T&& arg, Args&&...args)
{func(std::forward(args)... );
}
其中std::forward
實現(xiàn)了針對左右值的參數(shù),能保證被轉(zhuǎn)發(fā)參數(shù)的左、右值屬性不變,即完美轉(zhuǎn)發(fā)(perfect forwarding)。
在C++11中,完美轉(zhuǎn)發(fā)支持:
T&& 參數(shù)名
,那么,函數(shù)既可以接受左值引用,又可以接受右值引用。std::forward(參數(shù))
,用于轉(zhuǎn)發(fā)參數(shù),如果 參數(shù)是一個右值,轉(zhuǎn)發(fā)之后仍是右值引用;如果參數(shù)是一個左值,轉(zhuǎn)發(fā)之后仍是左值引用。使用示例如下:
void fun1(int& i)//如果參數(shù)是左值
{cout<< "左值 = "<< i<< endl;
}
void fun1(int&& i)//如果參數(shù)是右值
{cout<< "右值 = "<< i<< endl;
}
templatevoid func(T&& ii)
{fun1(std::forward(ii));
}
完美轉(zhuǎn)發(fā)的語法比較簡單,至于其原理本文暫時不深入研究。😝
最后本文部分參考自文章
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧