C++深入右值引用之移動(dòng)語(yǔ)義與完美轉(zhuǎn)發(fā)(最新推薦)
1. 左值與右值
左值和右值不是新概念,C++98就有,但C++11賦予了它們更重要的地位。
- 左值:可以取地址的表達(dá)式,有持久狀態(tài)。變量、解引用的指針、數(shù)組元素都是左值。
- 右值:不能取地址。字面量、臨時(shí)對(duì)象、表達(dá)式求值的中間結(jié)果。
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10; // *p 是左值
string s("hello");
s[0] = 'x'; // s[0] 是左值
10; // 右值
x + y; // 右值
string("hello"); // 右值
// cout << &10; // 錯(cuò)誤:右值不能取地址C++11進(jìn)一步細(xì)化了:右值分為純右值(字面量、臨時(shí)對(duì)象等)和將亡值(move返回的右值引用)。泛左值包含左值和將亡值。這些概念不必死記,關(guān)鍵是記住核心區(qū)別:能否取地址。
2. 左值引用與右值引用
左值引用(T&)給左值取別名,右值引用(T&&)給右值取別名。
int& r1 = b; // 左值引用綁定左值
int&& rr1 = 10; // 右值引用綁定右值
double&& rr2 = x + y;
string&& rr3 = string("hello");幾個(gè)規(guī)則:
- 左值引用不能直接綁定右值,但const左值引用可以。
- 右值引用不能直接綁定左值,但可以通過(guò)
std::move轉(zhuǎn)換。
const int& cr = 10; // OK int&& rr4 = move(b); // OK,move后的b仍可使用但資源已被取走
move()本質(zhì)上是一個(gè)強(qiáng)制類(lèi)型轉(zhuǎn)換:static_cast<remove_reference_t<T>&&>(arg)。它本身不移動(dòng)任何東西,只是給了你一個(gè)右值引用,真正的動(dòng)作由移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值完成。
有一個(gè)容易困惑的點(diǎn):右值引用變量本身的表達(dá)式屬性是左值。因?yàn)樗忻帧⒖梢匀〉刂贰?/p>
int&& rr = 10; cout << &rr << endl; // 可以取地址,rr是左值 int& lr = rr; // OK,rr作為左值可以綁定左值引用 // int&& rr2 = rr; // 錯(cuò)誤,rr是左值,不能直接綁定右值引用 int&& rr2 = move(rr); // OK
這個(gè)設(shè)計(jì)看似別扭,但恰恰是移動(dòng)語(yǔ)義能正常工作的基礎(chǔ)——后面會(huì)看到。
3. 移動(dòng)構(gòu)造與移動(dòng)賦值
左值引用已經(jīng)在函數(shù)傳參和返回值中減少了大部分拷貝,但有一種場(chǎng)景它搞不定:返回局部對(duì)象。
string addStrings(string num1, string num2) {
string str;
// ... 構(gòu)建str ...
return str; // str是局部對(duì)象,函數(shù)結(jié)束就銷(xiāo)毀,不能傳引用返回
}C++98被迫拷貝,C++11通過(guò)移動(dòng)語(yǔ)義解決了這個(gè)問(wèn)題:既然str馬上就要銷(xiāo)毀,把它的資源“偷”過(guò)來(lái)就行,沒(méi)必要深拷貝。
移動(dòng)構(gòu)造和移動(dòng)賦值接受一個(gè)右值引用參數(shù),核心操作是交換資源,而不是復(fù)制。
namespace demo {
class string {
public:
string(const char* str = "")
: _size(strlen(str)), _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷貝構(gòu)造
string(const string& s) {
// 深拷貝...
}
// 移動(dòng)構(gòu)造
string(string&& s) {
swap(s); // 直接把s的資源換到自己身上
}
// 拷貝賦值
string& operator=(const string& s) {
// 深拷貝...
}
// 移動(dòng)賦值
string& operator=(string&& s) {
swap(s);
return *this;
}
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
~string() { delete[] _str; }
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}使用效果:
demo::string s1("hello");
demo::string s2 = s1; // 拷貝構(gòu)造
demo::string s3 = demo::string("world"); // 移動(dòng)構(gòu)造(臨時(shí)對(duì)象)
demo::string s4 = move(s1); // 移動(dòng)構(gòu)造(顯式move)編譯器還會(huì)進(jìn)一步優(yōu)化。在VS2022的release模式下,demo::string s3 = demo::string("world");可能直接被優(yōu)化成一次原地構(gòu)造,移動(dòng)構(gòu)造都不會(huì)調(diào)用——這就是返回值優(yōu)化(RVO)。
返回值場(chǎng)景中,如果提供了移動(dòng)構(gòu)造,沒(méi)有優(yōu)化時(shí)編譯器會(huì)優(yōu)先選擇移動(dòng)而不是拷貝,顯著提高效率。
4. 移動(dòng)語(yǔ)義在容器中的應(yīng)用
C++11之后,STL容器的push_back、insert都增加了右值引用版本:
void push_back(const T& x); // 左值版本,內(nèi)部拷貝 void push_back(T&& x); // 右值版本,內(nèi)部移動(dòng)
當(dāng)傳入左值,走拷貝;傳入右值,走移動(dòng)。
list<demo::string> lt;
demo::string s1("hello");
lt.push_back(s1); // 拷貝
lt.push_back(move(s1)); // 移動(dòng)
lt.push_back("world"); // 移動(dòng)(臨時(shí)string)
lt.push_back(demo::string("world")); // 移動(dòng)自定義容器也可以實(shí)現(xiàn)相應(yīng)的重載,內(nèi)部在節(jié)點(diǎn)構(gòu)造時(shí)用move把參數(shù)轉(zhuǎn)成右值。
5. 完美轉(zhuǎn)發(fā)與引用折疊
考慮一個(gè)模板函數(shù),想要把參數(shù)原樣轉(zhuǎn)發(fā)給另一個(gè)函數(shù):
template<class T>
void relay(T&& t) {
func(t); // t是左值,永遠(yuǎn)調(diào)用func的左值版本,即使傳進(jìn)來(lái)的是右值
}問(wèn)題在于:t作為右值引用變量,表達(dá)式屬性是左值,所以func(t)調(diào)不到右值重載版本。要保留參數(shù)的原始值類(lèi)別,需要完美轉(zhuǎn)發(fā):
template<class T>
void relay(T&& t) {
func(forward<T>(t));
}forward的實(shí)現(xiàn)依賴(lài)于引用折疊規(guī)則。C++不允許直接定義引用的引用(int& &),但在模板和類(lèi)型推導(dǎo)中可以出現(xiàn)。規(guī)則只有一條:右值引用的右值引用折疊成右值引用,其余全部折疊成左值引用。
typedef int& lref; typedef int&& rref; lref& r1 = n; // int& & → int& lref&& r2 = n; // int& && → int& rref& r3 = n; // int&& & → int& rref&& r4 = 1; // int&& && → int&&
在relay中,T的推導(dǎo)結(jié)果配合引用折疊,實(shí)現(xiàn)了“左值傳左值,右值傳右值”:
- 實(shí)參是左值
int a→T推導(dǎo)為int&→T&&折疊為int& - 實(shí)參是右值
10→T推導(dǎo)為int→T&&為int&&
forward內(nèi)部也是通過(guò)static_cast<T&&>實(shí)現(xiàn)的,配合引用折疊,左值情況返回左值引用,右值情況返回右值引用。這樣就把參數(shù)的原始屬性一路傳下去了。
6. 默認(rèn)移動(dòng)構(gòu)造與移動(dòng)賦值
編譯器在特定條件下會(huì)自動(dòng)生成移動(dòng)構(gòu)造和移動(dòng)賦值:
- 如果用戶(hù)沒(méi)有定義拷貝構(gòu)造、拷貝賦值、析構(gòu)函數(shù)中的任何一個(gè),編譯器會(huì)嘗試自動(dòng)生成移動(dòng)構(gòu)造和移動(dòng)賦值。
- 對(duì)于內(nèi)置類(lèi)型成員,逐字節(jié)拷貝;對(duì)于自定義類(lèi)型成員,如果能移動(dòng)就移動(dòng),否則拷貝。
一旦你提供了移動(dòng)構(gòu)造或移動(dòng)賦值,編譯器就不會(huì)自動(dòng)生成拷貝構(gòu)造和拷貝賦值。所以如果你想兩者都有,需要自己顯式聲明,或者用= default:
class Person {
public:
Person(const Person&) = default; // 顯式要拷貝
Person(Person&&) = default; // 顯式要移動(dòng)
};也可以用= delete禁止某個(gè)函數(shù)。
移動(dòng)語(yǔ)義是C++11最核心的特性之一。它讓“快”從“避免拷貝”的編碼技巧,變成了語(yǔ)言級(jí)別的優(yōu)化路徑。
到此這篇關(guān)于C++深入右值引用之移動(dòng)語(yǔ)義與完美轉(zhuǎn)發(fā)(最新推薦)的文章就介紹到這了,更多相關(guān)C++右值引用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言數(shù)組棧實(shí)現(xiàn)模板
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言數(shù)組棧實(shí)現(xiàn)模板,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
淺析C語(yǔ)言中strtol()函數(shù)與strtoul()函數(shù)的用法
這篇文章主要介紹了淺析C語(yǔ)言中strtol()函數(shù)與strtoul()函數(shù)的用法,注意其將字符串轉(zhuǎn)換成long型的區(qū)別,需要的朋友可以參考下2015-08-08
C++中臨時(shí)對(duì)象的常見(jiàn)產(chǎn)生情況及其解決的方案
這篇文章主要是探討常見(jiàn)的臨時(shí)對(duì)象產(chǎn)生的情況,及其如何避免和解決這種臨時(shí)對(duì)象產(chǎn)生的方式。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
VCPKG安裝和使用教程(經(jīng)驗(yàn)總結(jié))
這篇文章主要介紹了VCPKG安裝和使用教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01
C語(yǔ)言中strlen() strcpy() strcat() strcmp()函數(shù)的實(shí)現(xiàn)方法
這篇文章主要介紹了C語(yǔ)言中strlen() strcpy() strcat() strcmp()函數(shù)的實(shí)現(xiàn)方法,需要的朋友可以參考下2017-08-08

