C++左值引用、右值引用應(yīng)用場景分析
一、右值引用的意義
在 C++ 的編程世界中,對象的拷貝與資源管理一直是性能優(yōu)化的核心痛點。在 C++11 標(biāo)準(zhǔn)之前,我們編寫的代碼中隱藏著大量臨時對象的深拷貝開銷:比如函數(shù)返回一個非引用類型的對象、容器插入臨時對象時,都會觸發(fā)拷貝構(gòu)造函數(shù),完成堆內(nèi)存的重新分配與數(shù)據(jù)復(fù)制,而臨時對象在表達式結(jié)束后就會被銷毀,這就造成了完全無意義的內(nèi)存操作與性能浪費。
舉個最簡單的例子:
std::string str = "hello" + " world";,表達式會生成一個臨時的 string 對象,再通過拷貝構(gòu)造把數(shù)據(jù)復(fù)制給 str,隨后臨時對象被銷毀。如果字符串很長,這個深拷貝的開銷是完全可以避免的。而 C++11 引入的右值引用,正是為了解決這個問題。它不僅重新定義了 C++ 的資源管理哲學(xué),讓我們可以對即將銷毀的臨時對象的資源進行轉(zhuǎn)移而非拷貝,還支撐了泛型編程中的完美轉(zhuǎn)發(fā),成為現(xiàn)代 C++ 不可或缺的核心特性。
二、基礎(chǔ):理解左值與右值
1. 左值(Lvalue,Locator Value)
左值的核心定義是:可以通過
&取地址運算符獲取其內(nèi)存地址、擁有持久生命周期的表達式。簡單來說,左值就是一個 “有名字、有固定內(nèi)存地址” 的對象,你可以找到它的存儲位置,除了 const 修飾的左值外,都可以修改其內(nèi)容。左值既可以放在賦值號的左邊,也可以放在賦值號的右邊(作為右值使用)。
常見的左值場景:
- 普通變量、const 修飾的變量(const 左值不可修改,但依然可以取地址,屬于左值)
- 對指針解引用的結(jié)果
*ptr - 數(shù)組的元素
arr[index] - 結(jié)構(gòu)體 / 類的非靜態(tài)成員變量
- 返回左值引用的函數(shù)調(diào)用
- 前綴自增 / 自減表達式
++i、--i
int main()
{
int i = 0;
i = 1; // i是變量,可修改左值,能放在賦值號左側(cè)
const int ci = 5;
// ci = 6; // 報錯:const左值不可修改,但&ci可獲取地址,仍是左值
int* pi = &i;
*pi = 1; // *pi對指針解引用,左值
int arr[5] = {0};
arr[0] = 1; // 數(shù)組元素,左值
struct { int m_a; } st, *pst = &st;
st.m_a = 1; // 結(jié)構(gòu)體成員,左值
pst->m_a = 2;
return 0;
}2. 右值(Rvalue,Read Value)
C++11 之后,右值被細分為純右值(prvalue,Pure Rvalue) 和將亡值(xvalue,eXpiring Value) 兩類,二者共同構(gòu)成了右值。
右值的核心定義是:無法通過
&取地址、生命周期短暫的臨時表達式結(jié)果,它代表的是一個值,而非值所在的存儲位置。右值只能放在賦值號的右側(cè),不能放在左側(cè)。
2.1 純右值(prvalue)
純右值是 C++98 標(biāo)準(zhǔn)中傳統(tǒng)意義上的右值,核心是 “字面量、臨時計算結(jié)果”,沒有持久的存儲地址。
常見的純右值場景:
- 除字符串字面量外的字面常量,如
42、'a'、true(字符串字面量是 const char 數(shù)組類型,屬于左值) - 算術(shù)、邏輯、關(guān)系運算符的求值結(jié)果,如
1+2、a>10、a&&b - 非引用類型返回值的函數(shù)調(diào)用結(jié)果
- 后置自增 / 自減表達式
i++、i-- - lambda 表達式
2.2 將亡值(xvalue)
將亡值是 C++11 為了配合右值引用新增的概念,核心是 “資源即將被轉(zhuǎn)移、生命周期即將結(jié)束的對象”,它是連接左值和右值的橋梁。
將亡值雖然可以通過特殊方式獲取地址,但它的核心使命是完成資源轉(zhuǎn)移,使用后就會被銷毀,因此歸屬于右值范疇。
常見的將亡值場景:
std::move()的返回結(jié)果- 返回右值引用類型的函數(shù)調(diào)用結(jié)果
- 類型轉(zhuǎn)換為右值引用的表達式結(jié)果
int getNum()
{
return 100;
}
int main()
{
int i = 0, c = 0;
i = 42; // 字面值42,純右值
c = 'a'; // 字面值'a',純右值
i = 1 + 2; // 算術(shù)表達式結(jié)果,純右值
i = (c != 'a'); // 邏輯表達式結(jié)果,純右值
i = getNum(); // 非引用返回值,純右值
int&& rri = std::move(i);// std::move(i)的結(jié)果,將亡值,右值
return 0;
}3. 左值與右值的核心區(qū)別
| 核心特性 | 左值 | 右值(純右值 + 將亡值) |
|---|---|---|
| 內(nèi)存地址 | 可通過&獲取穩(wěn)定的內(nèi)存地址 | 無法直接通過&獲取地址 |
| 生命周期 | 持久,隨變量 / 對象的作用域存在 | 臨時,表達式結(jié)束后即銷毀(除非被引用綁定) |
| 可修改性 | 非 const 左值可修改,const 左值不可修改 | 不可直接修改 |
| 賦值位置 | 可放在賦值號左側(cè)、右側(cè) | 只能放在賦值號右側(cè) |
| 引用綁定 | 可被左值引用、const 左值引用綁定 | 只能被 const 左值引用、右值引用綁定 |
三、右值引用的定義與語法規(guī)則
1. 左值引用的回顧與局限
在 C++11 之前,我們使用的引用都是左值引用,用&符號聲明,本質(zhì)是給變量起一個 “別名”,底層通過指針實現(xiàn),必須在定義時初始化,且初始化后無法重新綁定到其他對象。
左值引用的核心局限:非 const 左值引用只能綁定到左值,無法綁定到右值。只有 const 左值引用是個例外,它可以同時綁定左值和右值,但 const 修飾決定了我們無法通過它修改綁定的對象。
int main()
{
int a = 10;
int& ra = a; // 正確:非const左值引用綁定左值
// int& rb = 10; // 報錯:非const左值引用無法綁定右值
const int& rca = a; // 正確:const左值引用綁定左值
const int& rcb = 10; // 正確:const左值引用綁定右值
// rcb = 20; // 報錯:const引用無法修改綁定對象
return 0;
}這個局限帶來了兩個核心問題:
- 無法對臨時的右值對象進行修改和資源復(fù)用,只能通過拷貝使用
- 泛型編程中,無法區(qū)分傳入的參數(shù)是左值還是右值,無法實現(xiàn)精準(zhǔn)的參數(shù)轉(zhuǎn)發(fā)
而右值引用的出現(xiàn),完美解決了這兩個問題。
2. 右值引用的語法與本質(zhì)
右值引用是 C++11 新增的引用類型,用&&符號聲明,核心是專門綁定到右值(臨時對象、將亡值)的引用。
基礎(chǔ)語法:
類型&& 引用名 = 右值表達式;
右值引用的核心規(guī)則:
- 必須在定義時初始化,且初始化后無法重新綁定到其他對象
- 只能直接綁定到右值,無法直接綁定到左值
- 綁定到右值后,會延長該臨時右值的生命周期,生命周期與右值引用變量一致
- 可以通過右值引用修改綁定的右值對象
- 右值引用變量本身是左值(有名字、可通過
&取地址),這是初學(xué)者最容易踩的坑
#include <iostream>
using namespace std;
int getNum()
{
return 100;
}
void func(int&& rri)
{
rri = 0;
}
int main()
{
// 1. 右值引用必須綁定右值
int&& ri0 = 42; // 正確:綁定字面量純右值
int&& ri1 = 1 + 2; // 正確:綁定表達式結(jié)果純右值
int&& ri2 = getNum(); // 正確:綁定函數(shù)返回值純右值
// int&& ri3 = ri0; // 報錯:ri0是右值引用變量,本身是左值,無法綁定
// 2. 可通過右值引用修改綁定的對象
ri0 = 100;
cout << "ri0 = " << ri0 << endl; // 輸出:ri0 = 100
// 3. 可獲取右值引用的地址(證明它本身是左值)
int* pi = &ri0;
*pi = 200;
cout << "*pi = " << *pi << ", ri0 = " << ri0 << endl; // 輸出:*pi = 200, ri0 = 200
// 4. 右值引用作為函數(shù)參數(shù),接收右值
func(1 + 2);
return 0;
}3. std::move ()做了什么?
std::move()實現(xiàn)了資源的移動?這是錯誤的。
std::move()的本質(zhì):不做任何資源移動、不生成任何機器碼,只是無條件地將一個左值強制轉(zhuǎn)換為右值引用類型(將亡值),僅此而已。
它的唯一作用,就是讓左值可以被右值引用綁定,從而為后續(xù)的資源轉(zhuǎn)移提供可能。真正的資源轉(zhuǎn)移,是在類的移動構(gòu)造函數(shù)、移動賦值運算符中完成的,std::move()只是打開了 “資源轉(zhuǎn)移” 的入口。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "Hello World!";
cout << "move前:str = " << str << endl;
// std::move將str左值轉(zhuǎn)為右值,觸發(fā)string的移動構(gòu)造函數(shù)
string tmp = std::move(str);
cout << "move后:str = " << str << endl; // 結(jié)果未定義,標(biāo)準(zhǔn)僅保證str可析構(gòu)
cout << "move后:tmp = " << tmp << endl;
return 0;
}注:
std::move()之后的原對象,其資源已經(jīng)被轉(zhuǎn)移,處于 “有效但未定義” 的狀態(tài),只能對其進行析構(gòu)或重新賦值,絕對不能再訪問其內(nèi)部資源,否則會出現(xiàn)未定義行為。
4. 左值引用 vs 右值引用
| 特性 | 左值引用(Type&) | 右值引用(Type&&) |
|---|---|---|
| 聲明符號 | & | && |
| 初始化要求 | 必須初始化,綁定后不可重新綁定 | 必須初始化,綁定后不可重新綁定 |
| 直接綁定對象 | 非 const 左值引用:僅左值const 左值引用:左值 + 右值 | 僅右值(純右值 + 將亡值),左值需通過 std::move 轉(zhuǎn)換后綁定 |
| 綁定對象的修改性 | 非 const 左值引用:可修改const 左值引用:不可修改 | 可修改綁定的右值對象(const 右值引用除外) |
| 本身屬性 | 左值 | 有名字的右值引用變量是左值,匿名右值引用是右值 |
| 核心應(yīng)用場景 | 函數(shù)參數(shù)傳遞、避免拷貝、返回左值對象 | 移動語義、完美轉(zhuǎn)發(fā)、資源所有權(quán)轉(zhuǎn)移 |
| 引用折疊 | 與任何引用折疊均為左值引用 | 僅與右值引用折疊為右值引用,其余均為左值引用 |
| 能否為 nullptr | 不能,必須綁定合法對象 | 不能,必須綁定合法對象 |
四、右值引用的兩大核心應(yīng)用場景
1. 移動語義:告別無意義的深拷貝,實現(xiàn)資源高效轉(zhuǎn)移
移動語義是右值引用最核心的價值,它的核心思想是:對于即將銷毀的對象,不做資源的深拷貝,而是直接將其資源的所有權(quán) “轉(zhuǎn)移” 到新對象中,避免了內(nèi)存分配、數(shù)據(jù)復(fù)制和內(nèi)存釋放的巨大開銷。
移動語義的實現(xiàn),依賴于移動構(gòu)造函數(shù)和移動賦值運算符,二者的參數(shù)都是右值引用類型。
1.1 無移動語義的痛點:深拷貝的性能浪費
先實現(xiàn)一個簡單的字符串類,看看沒有移動語義時,臨時對象帶來的性能開銷:
#include <iostream>
#include <cstring>
using namespace std;
class MyString
{
public:
char* data;
size_t len;
// 普通構(gòu)造函數(shù)
MyString(const char* str)
{
len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
cout << "構(gòu)造函數(shù):分配內(nèi)存,地址=" << (void*)data << ",內(nèi)容=" << data << endl;
}
// 拷貝構(gòu)造函數(shù)(深拷貝)
MyString(const MyString& other)
{
len = other.len;
data = new char[len + 1];
strcpy(data, other.data);
cout << "拷貝構(gòu)造函數(shù):深拷貝內(nèi)存,新地址=" << (void*)data << ",內(nèi)容=" << data << endl;
}
// 拷貝賦值運算符(深拷貝)
MyString& operator=(const MyString& other)
{
if (this == &other)
{
return *this;
}
// 釋放自身原有資源
delete[] data;
// 深拷貝源對象資源
len = other.len;
data = new char[len + 1];
strcpy(data, other.data);
cout << "拷貝賦值運算符:深拷貝內(nèi)存,新地址=" << (void*)data << ",內(nèi)容=" << data << endl;
return *this;
}
// 析構(gòu)函數(shù)
~MyString()
{
if (data)
{
cout << "析構(gòu)函數(shù):釋放內(nèi)存,地址=" << (void*)data << ",內(nèi)容=" << data << endl;
delete[] data;
}
else
{
cout << "析構(gòu)函數(shù):資源已轉(zhuǎn)移,無需釋放" << endl;
}
}
};
MyString createString()
{
MyString str("Hello C++");
return str; // 返回臨時對象,觸發(fā)拷貝構(gòu)造
}
int main()
{
MyString str1("Hello World");
MyString str2 = str1; // 觸發(fā)拷貝構(gòu)造
MyString str3 = createString(); // 臨時對象觸發(fā)拷貝構(gòu)造,隨后銷毀
return 0;
}此處,每一次對象拷貝都會觸發(fā)深拷貝,重新分配內(nèi)存、復(fù)制數(shù)據(jù),臨時對象創(chuàng)建后很快被銷毀,造成了巨大的性能浪費。
1.2 基于右值引用實現(xiàn)移動語義
我們給 MyString 類添加移動構(gòu)造函數(shù)和移動賦值運算符,通過右值引用實現(xiàn)資源轉(zhuǎn)移:
#include <iostream>
#include <cstring>
using namespace std;
class MyString
{
public:
char* data;
size_t len;
// 普通構(gòu)造函數(shù)
MyString(const char* str)
{
len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
cout << "構(gòu)造函數(shù):分配內(nèi)存,地址=" << (void*)data << ",內(nèi)容=" << data << endl;
}
// 拷貝構(gòu)造函數(shù)(深拷貝)
MyString(const MyString& other)
{
len = other.len;
data = new char[len + 1];
strcpy(data, other.data);
cout << "拷貝構(gòu)造函數(shù):深拷貝內(nèi)存,新地址=" << (void*)data << ",內(nèi)容=" << data << endl;
}
// 移動構(gòu)造函數(shù)(右值引用參數(shù),資源轉(zhuǎn)移)
// noexcept關(guān)鍵字:告訴編譯器該函數(shù)不會拋出異常,STL容器會優(yōu)先調(diào)用
MyString(MyString&& other) noexcept
{
// 直接轉(zhuǎn)移源對象的資源,無需深拷貝
data = other.data;
len = other.len;
// 源對象指針置空,避免析構(gòu)時釋放資源
other.data = nullptr;
other.len = 0;
cout << "移動構(gòu)造函數(shù):轉(zhuǎn)移資源,目標(biāo)地址=" << (void*)data << endl;
}
// 拷貝賦值運算符(深拷貝)
MyString& operator=(const MyString& other)
{
if (this == &other)
{
return *this;
}
delete[] data;
len = other.len;
data = new char[len + 1];
strcpy(data, other.data);
cout << "拷貝賦值運算符:深拷貝內(nèi)存,新地址=" << (void*)data << ",內(nèi)容=" << data << endl;
return *this;
}
// 移動賦值運算符(右值引用參數(shù),資源轉(zhuǎn)移)
MyString& operator=(MyString&& other) noexcept
{
if (this == &other)
{
return *this;
}
// 釋放自身原有資源
delete[] data;
// 轉(zhuǎn)移源對象資源
data = other.data;
len = other.len;
// 源對象指針置空
other.data = nullptr;
other.len = 0;
cout << "移動賦值運算符:轉(zhuǎn)移資源,目標(biāo)地址=" << (void*)data << endl;
return *this;
}
// 析構(gòu)函數(shù)
~MyString()
{
if (data)
{
cout << "析構(gòu)函數(shù):釋放內(nèi)存,地址=" << (void*)data << ",內(nèi)容=" << data << endl;
delete[] data;
}
else
{
cout << "析構(gòu)函數(shù):資源已轉(zhuǎn)移,無需釋放" << endl;
}
}
};
MyString createString()
{
MyString str("Hello C++");
return str;
}
int main()
{
MyString str1("Hello World");
MyString str2 = std::move(str1); // 觸發(fā)移動構(gòu)造,轉(zhuǎn)移str1的資源
MyString str3 = createString(); // 臨時對象是右值,觸發(fā)移動構(gòu)造
str3 = MyString("Hello Move"); // 臨時對象觸發(fā)移動賦值
return 0;
}此處,移動構(gòu)造 / 移動賦值沒有做任何深拷貝,只是轉(zhuǎn)移了指針的指向,沒有新的內(nèi)存分配,性能得到了提升。
注:
- 移動構(gòu)造 / 移動賦值的參數(shù)必須是非 const 右值引用,因為我們需要修改源對象的指針,將其置空
- 強烈建議給移動構(gòu)造 / 移動賦值添加
noexcept關(guān)鍵字,STL 容器(如 vector)在擴容時,只有移動構(gòu)造被聲明為 noexcept,才會優(yōu)先調(diào)用移動構(gòu)造,否則會保守地調(diào)用拷貝構(gòu)造函數(shù) - 當(dāng)類沒有自定義拷貝構(gòu)造、拷貝賦值、析構(gòu)函數(shù)時,編譯器會自動生成默認的移動構(gòu)造和移動賦值函數(shù);如果自定義了上述函數(shù),編譯器不會自動生成,需要手動實現(xiàn)
1.3 移動語義的常見場景
- STL 容器的優(yōu)化:C++11 之后,所有 STL 容器都實現(xiàn)了移動構(gòu)造和移動賦值,使用
std::move()轉(zhuǎn)移容器可以避免全量拷貝,比如vector<int> vec2 = std::move(vec1);時間復(fù)雜度從 O (n) 降到 O (1) - emplace 系列函數(shù):
vector::emplace_back()、map::emplace()等函數(shù),通過完美轉(zhuǎn)發(fā)直接在容器內(nèi)存中構(gòu)造對象,避免了臨時對象的拷貝和移動,性能優(yōu)于push_back()/insert() - 智能指針的資源轉(zhuǎn)移:
std::unique_ptr無法拷貝,只能通過移動語義轉(zhuǎn)移所有權(quán),正是基于右值引用實現(xiàn) - 函數(shù)返回大對象:返回大容器、大對象時,編譯器會優(yōu)先進行 RVO/NRVO 返回值優(yōu)化,優(yōu)化失效時會自動調(diào)用移動構(gòu)造,無需手動加
std::move()
2. 完美轉(zhuǎn)發(fā)
完美轉(zhuǎn)發(fā)是右值引用的第二大核心應(yīng)用,它的核心目標(biāo)是:在模板函數(shù)中,將參數(shù)原封不動地轉(zhuǎn)發(fā)給另一個函數(shù),保留參數(shù)的左值 / 右值屬性、const/volatile 修飾符。
簡單來說,傳入的是左值,轉(zhuǎn)發(fā)后還是左值;傳入的是右值,轉(zhuǎn)發(fā)后還是右值,不會因為轉(zhuǎn)發(fā)而改變參數(shù)的類型屬性,這就是 “完美” 的含義。
2.1 完美轉(zhuǎn)發(fā)的底層支撐
完美轉(zhuǎn)發(fā)的實現(xiàn),依賴三個 C++11 的核心特性:
- 右值引用
- 萬能引用(轉(zhuǎn)發(fā)引用)
- 引用折疊規(guī)則
- std::forward()
2.1.1 萬能引用(轉(zhuǎn)發(fā)引用)
萬能引用是指模板函數(shù)中,形如T&&的參數(shù),它既可以綁定左值,也可以綁定右值,同時保留參數(shù)的所有類型屬性。
注意:只有發(fā)生模板參數(shù)推導(dǎo)時,T&&才是萬能引用;如果是具體類型的Type&&,只是普通的右值引用,只能綁定右值。
// 模板參數(shù)推導(dǎo),T&&是萬能引用
template<typename T>
void func(T&& arg)
{
}
// 具體類型,int&&是普通右值引用
void func2(int&& arg)
{
}
int main()
{
int a = 10;
func(a); // 正確:萬能引用綁定左值
func(10); // 正確:萬能引用綁定右值
// func2(a); // 報錯:普通右值引用無法綁定左值
func2(10); // 正確:普通右值引用綁定右值
return 0;
}
2.1.2 引用折疊規(guī)則
C++ 不允許引用的引用,但在模板參數(shù)推導(dǎo)時,會出現(xiàn)引用嵌套的情況,此時編譯器會執(zhí)行引用折疊,規(guī)則如下:
- 左值引用
&與 任何引用(&/&&)折疊,最終結(jié)果都是左值引用& - 右值引用
&&與 右值引用&&折疊,最終結(jié)果才是右值引用&&
| 嵌套類型 | 折疊結(jié)果 |
|---|---|
| T& & | T& |
| T& && | T& |
| T&& & | T& |
| T&& && | T&& |
這個規(guī)則,讓萬能引用可以同時適配左值和右值:
- 當(dāng)傳入左值
int&時,T 被推導(dǎo)為int&,T&& =int& &&,折疊為int&,左值引用 - 當(dāng)傳入右值
int&&時,T 被推導(dǎo)為int,T&& =int&&,右值引用
2.1.3 std::forward () 的作用
std::forward()是實現(xiàn)完美轉(zhuǎn)發(fā)的核心函數(shù),它的作用是有條件地進行類型轉(zhuǎn)換:
- 當(dāng)傳入的是左值時,不做任何轉(zhuǎn)換,依然返回左值
- 當(dāng)傳入的是右值時,轉(zhuǎn)換為右值引用,返回右值
和std::move()的無條件轉(zhuǎn)右值不同,std::forward()是 “按需轉(zhuǎn)換”,完美保留參數(shù)的原始屬性。
2.2 完美轉(zhuǎn)發(fā)的示例
我們通過一個完整的例子,看看完美轉(zhuǎn)發(fā)的效果,以及不使用完美轉(zhuǎn)發(fā)的問題:
#include <iostream>
#include <utility>
using namespace std;
// 重載函數(shù),區(qū)分左值和右值參數(shù)
void process(int& x)
{
cout << "左值引用版本被調(diào)用,值=" << x << endl;
}
void process(int&& x)
{
cout << "右值引用版本被調(diào)用,值=" << x << endl;
}
// 不使用完美轉(zhuǎn)發(fā)的模板函數(shù)
template<typename T>
void noForward(T&& arg)
{
cout << "無完美轉(zhuǎn)發(fā):";
process(arg); // arg是有名字的變量,本身是左值,永遠調(diào)用左值版本
}
// 使用完美轉(zhuǎn)發(fā)的模板函數(shù)
template<typename T>
void withForward(T&& arg)
{
cout << "有完美轉(zhuǎn)發(fā):";
process(std::forward<T>(arg)); // 完美轉(zhuǎn)發(fā),保留原始左值/右值屬性
}
int main()
{
int a = 5;
cout << "===== 傳入左值a =====" << endl;
noForward(a);
withForward(a);
cout << "\n===== 傳入右值10 =====" << endl;
noForward(10);
withForward(10);
return 0;
}運行結(jié)果:
===== 傳入左值a =====
無完美轉(zhuǎn)發(fā):左值引用版本被調(diào)用,值=5
有完美轉(zhuǎn)發(fā):左值引用版本被調(diào)用,值=5===== 傳入右值10 =====
無完美轉(zhuǎn)發(fā):左值引用版本被調(diào)用,值=10
有完美轉(zhuǎn)發(fā):右值引用版本被調(diào)用,值=10
可以看到,不使用std::forward()時,無論傳入左值還是右值,都會調(diào)用左值版本的 process 函數(shù),因為函數(shù)內(nèi)部的 arg 是有名字的變量,本身是左值。而使用完美轉(zhuǎn)發(fā)后,參數(shù)的左值 / 右值屬性被完美保留,正確調(diào)用了對應(yīng)的重載函數(shù)。
2.3 完美轉(zhuǎn)發(fā)的常見場景
- STL 容器的 emplace 系列函數(shù):直接在容器內(nèi)存中構(gòu)造對象,將用戶傳入的參數(shù)完美轉(zhuǎn)發(fā)給對象的構(gòu)造函數(shù),保留所有類型屬性
- std::bind/std::function:實現(xiàn)函數(shù)對象的封裝和參數(shù)轉(zhuǎn)發(fā),底層依賴完美轉(zhuǎn)發(fā)
- 線程庫 std::thread:將線程函數(shù)的參數(shù)完美轉(zhuǎn)發(fā)到線程執(zhí)行上下文
- 泛型編程中的中間轉(zhuǎn)發(fā)函數(shù):需要將參數(shù)透傳給其他函數(shù)的模板場景
五、高頻踩坑點與實踐建議
1. 高頻踩坑點
坑 1:std::move () 之后繼續(xù)使用原對象
std::move()之后,原對象的資源已經(jīng)被轉(zhuǎn)移,處于有效但未定義的狀態(tài),此時訪問其內(nèi)部資源(如 string 的字符、vector 的元素)會觸發(fā)未定義行為,只能對其進行析構(gòu)或重新賦值。
坑 2:對函數(shù)返回值使用 std::move ()
很多人會寫return std::move(obj);,這是典型的錯誤寫法。編譯器默認會對返回的局部對象執(zhí)行 RVO/NRVO 返回值優(yōu)化,直接在返回值地址構(gòu)造對象,零拷貝。而加了std::move()之后,會阻止編譯器的優(yōu)化,強制調(diào)用移動構(gòu)造,反而降低性能。
坑 3:誤以為右值引用變量本身是右值
有名字的右值引用變量,本身是左值,無法直接綁定到右值引用參數(shù)。如果需要在函數(shù)中轉(zhuǎn)發(fā)右值引用形參,必須使用std::move()或std::forward()。
void func(int&& x)
{
}
void test(int&& arg)
{
// func(arg); // 報錯:arg是右值引用變量,本身是左值
func(std::move(arg)); // 正確
}坑 4:給移動構(gòu)造 / 移動賦值加 const 修飾
移動構(gòu)造 / 移動賦值需要修改源對象的指針,將其置空,加了 const 之后無法修改源對象,只能做深拷貝,完全失去了移動語義的意義。const 右值引用幾乎沒有使用場景,不如直接用 const 左值引用。
坑 5:把普通右值引用當(dāng)成萬能引用
只有在模板參數(shù)推導(dǎo)場景下的T&&才是萬能引用,類成員函數(shù)的模板參數(shù)如果沒有推導(dǎo)過程,也不是萬能引用。
class Test
{
public:
// 這里的T&&不是萬能引用,因為類實例化時T已經(jīng)確定,沒有參數(shù)推導(dǎo)
template<typename T>
void func(T&& arg);
};2. 實踐建議
- 只在需要轉(zhuǎn)移資源的時候使用
std::move(),且確保 move 之后的對象不再使用 - 實現(xiàn)移動構(gòu)造 / 移動賦值時,必須添加
noexcept關(guān)鍵字,確保 STL 容器能優(yōu)先調(diào)用 - 函數(shù)返回局部對象時,不要加
std::move(),交給編譯器做返回值優(yōu)化 - 泛型編程中,需要轉(zhuǎn)發(fā)參數(shù)時,使用
std::forward()實現(xiàn)完美轉(zhuǎn)發(fā),不要用std::move() - 遵循 “三五法則”:如果自定義了析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)、拷貝賦值運算符中的任意一個,就應(yīng)該同時實現(xiàn)或禁用移動構(gòu)造、移動賦值運算符
- 對于不需要拷貝、只需要轉(zhuǎn)移所有權(quán)的對象(如 unique_ptr、文件句柄),禁用拷貝構(gòu)造和拷貝賦值,只實現(xiàn)移動構(gòu)造和移動賦
到此這篇關(guān)于C++左值引用、右值引用應(yīng)用場景分析的文章就介紹到這了,更多相關(guān)C++左值引用、右值引用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++中構(gòu)造函數(shù)與析構(gòu)函數(shù)的調(diào)用順序詳解
C++ 語言一直被批評太復(fù)雜了,很多細節(jié)的地方需要仔細推敲,甚至其構(gòu)造函數(shù)和析構(gòu)的調(diào)用順序也成為了一個讓人迷惑的問題,在此我做了簡單的總結(jié)。這篇文章主要介紹了C++中構(gòu)造函數(shù)與析構(gòu)函數(shù)的調(diào)用順序,需要的朋友可以參考借鑒。2017-01-01
C++實現(xiàn)LeetCode(23.合并k個有序鏈表)
這篇文章主要介紹了C++實現(xiàn)LeetCode(23.合并k個有序鏈表),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-07-07

