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

