C++右值引用
右值引用應(yīng)該是C++11
引入的一個非常重要的技術(shù)顷帖,因為它是移動語義(Move semantics)與完美轉(zhuǎn)發(fā)(Perfect forwarding)的基石:
- 移動語義:將內(nèi)存的所有權(quán)從一個對象轉(zhuǎn)移到另外一個對象绊谭,高效的移動用來替換效率低下的復(fù)制虽界,對象的移動語義需要實現(xiàn)移動構(gòu)造函數(shù)(move constructor)和移動賦值運(yùn)算符(move asssignment operator)析显。
- 完美轉(zhuǎn)發(fā):定義一個函數(shù)模板,該函數(shù)模板可以接收任意類型參數(shù),然后將參數(shù)轉(zhuǎn)發(fā)給其它目標(biāo)函數(shù)移斩,且保證目標(biāo)函數(shù)接受的參數(shù)其類型與傳遞給模板函數(shù)的類型相同。
左值與右值
在講解右值引用之前绢馍,你必須首先要區(qū)分兩個概念:左值與右值向瓷。但是精確講解清楚這兩個概念并不容易。首先舰涌,你要清楚左值與右值是C++中表達(dá)式的屬性猖任,在C++11
中,每個表達(dá)式有兩個屬性:類型(type瓷耙,除去引用特性朱躺,用于類型檢查)和值類型(value category刁赖,用于語法檢查,比如一個表達(dá)式結(jié)果是否能被賦值)室琢。值類型包括3個基本類型:lvalue
乾闰、prvalue
與xrvalue
。后兩者又統(tǒng)稱為rvalue
盈滴。lvalue
我們稱為左值涯肩,你可以將左值看成是一個可以獲取地址的量,它可以用來標(biāo)識一個對象或函數(shù)巢钓。rvalue
稱為右值病苗,你可以認(rèn)為所有不是左值的量就是右值,這是最簡單的解釋症汹。要準(zhǔn)確區(qū)分出右值中的prvalue
和xrvalue
并不容易:大概前者就是純粹的右值硫朦,比如字面量,后者指的是可以被重用的臨時對象背镇。如果你感興趣咬展,你可以訪問cppreference去細(xì)究。但是瞒斩,你只要能夠區(qū)分開左值與右值就夠了破婆。
左值引用
在C++11
之前就已經(jīng)有了左值引用,有時候我們簡稱為引用胸囱,其語法很簡單:
int x = 20;
int& rx = x; // 定義引用時必須初始化
但是引用也分為const引用與non-const引用祷舀,對于non-const引用,其只能用non-const左值來初始化:
int x = 20;
int& rx1 = x; // non-const引用可以被non-const左值初始化
const int y = 10;
const int& rx2 = y; // 非法:non-const引用不能被const左值初始化
int& rx3 = 10; // 非法:non-const引用不能被右值初始化
但是const引用限制就少了:
int x = 10;
const int cx = 20;
const int& rx1 = x; // const引用可以被non-const左值初始化
const int& rx2 = cx; // const引用可以被const左值初始化
const int& rx3 = 9; // const引用可以被右值初始化
理解上面并不難烹笔,因為你只要想著這樣初始化不會造成矛盾就好了裳扯,特別注意的是const左值引用可以接收右值(這點很重要,后面會說)谤职。
右值引用
在C++11
以前饰豺,右值被認(rèn)為是無用的資源,所以在C++11
中引入了右值引用允蜈,就是為了重用右值冤吨。定義右值引用需要使用&&
:
int&& rrx = 200;
右值引用一定不能被左值所初始化,只能用左值初始化:
int x = 20; // 左值
int&& rrx1 = x; // 非法:右值引用無法被左值初始化
const int&& rrx2 = x; // 非法:右值引用無法被左值初始化
那么為什么呢陷寝?因為右值引用的目的是為了延長用來初始化對象的生命周期,對于左值其馏,其生命周期與其作用域有關(guān)凤跑,你沒有必要去延長,這是我的理解叛复。既然是延長仔引,那么就出現(xiàn)了下面的情況:
int x = 20; // 左值
int&& rx = x * 2; // x*2的結(jié)果是一個右值扔仓,rx延長其生命周期
int y = rx + 2; // 因此你可以重用它:42
rx = 100; // 一旦你初始化一個右值引用變量,該變量就成為了一個左值咖耘,可以被賦值
這點很重要翘簇,初始化之后的右值引用將變成一個左值,如果是non-const還可以被賦值儿倒!
右值引用還可以用于函數(shù)參數(shù):
// 接收左值
void fun(int& lref)
{
cout << "l-value reference\n";
}
// 接收右值
void fun(int&& rref)
{
cout << "r-value reference\n";
}
int main()
{
int x = 10;
fun(x); // output: l-value reference
fun(10); // output: r-value reference
}
可以看到版保,函數(shù)參數(shù)要區(qū)分開右值引用與左值引用,這是兩個不同的重載版本夫否。還有彻犁,如果你定義了下面的函數(shù):
void fun(const int& clref)
{
cout << "l-value const reference\n";
}
但是其實它不僅可以接收左值,而且可以接收右值(如果你沒有提供接收右值引用的重載版本)凰慈。
移動語義
有了右值引用的概念汞幢,就可以理解移動語義了。前面說過微谓,一個對象的移動語義的實現(xiàn)是通過移動構(gòu)造函數(shù)與移動賦值運(yùn)算符來實現(xiàn)的森篷。所以,為了理解移動語義豺型,我們從一個對象出發(fā)仲智,下面創(chuàng)建一個動態(tài)數(shù)組類:
template <typename T>
class DynamicArray
{
public:
explicit DynamicArray(int size) :
m_size{ size }, m_array{ new T[size] }
{
cout << "Constructor: dynamic array is created!\n";
}
virtual ~DynamicArray()
{
delete[] m_array;
cout << "Destructor: dynamic array is destroyed!\n";
}
// 復(fù)制構(gòu)造函數(shù)
DynamicArray(const DynamicArray& rhs) :
m_size{ rhs.m_size }
{
m_array = new T[m_size];
for (int i = 0; i < m_size; ++i)
m_array[i] = rhs.m_array[i];
cout << "Copy constructor: dynamic array is created!\n";
}
// 復(fù)制賦值操作符
DynamicArray& operator=(const DynamicArray& rhs)
{
cout << "Copy assignment operator is called\n";
if (this == &rhs)
return *this;
delete[] m_array;
m_size = rhs.m_size;
m_array = new T[m_size];
for (int i = 0; i < m_size; ++i)
m_array[i] = rhs.m_array[i];
return *this;
}
// 索引運(yùn)算符
T& operator[](int index)
{
// 不進(jìn)行邊界檢查
return m_array[index];
}
const T& operator[](int index) const
{
return m_array[index];
}
int size() const { return m_size; }
private:
T* m_array;
int m_size;
};
我們通過在堆上動態(tài)分配內(nèi)存來實現(xiàn)動態(tài)數(shù)組類,類中實現(xiàn)復(fù)制構(gòu)造函數(shù)触创、復(fù)制賦值操作符以及索引操作符坎藐。假如我們定義一個生產(chǎn)動態(tài)數(shù)組的工廠函數(shù):
// 生產(chǎn)int動態(tài)數(shù)組的工廠函數(shù)
DynamicArray<int> arrayFactor(int size)
{
DynamicArray<int> arr{ size };
return arr;
}
然后我們用下面的代碼進(jìn)行測試:
int main()
{
{
DynamicArray<int> arr = arrayFactor(10);
}
return 0;
}
其輸出為:
Constructor: dynamic array is created!
Copy constructor: dynamic array is created!
Destructor: dynamic array is destroyed!
Destructor: dynamic array is destroyed!
此時,我們來解讀這個輸出哼绑。首先岩馍,你調(diào)用arrayFactor函數(shù),內(nèi)部創(chuàng)建了一個動態(tài)數(shù)組抖韩,所以普通構(gòu)造函數(shù)被調(diào)用蛀恩。然后將這個動態(tài)數(shù)組返回,但是這個對象是函數(shù)內(nèi)部的茂浮,函數(shù)外是無法獲得的双谆,所以要生成一個臨時對象,然后用這個動態(tài)數(shù)組初始化席揽,函數(shù)最終返回的是臨時對象顽馋。我們知道這個動態(tài)數(shù)組即將消亡,所以其是右值幌羞,那么在構(gòu)建臨時對象時寸谜,會調(diào)用復(fù)制構(gòu)造函數(shù)(沒有右值的版本,但是右值可以傳遞給const左值引用參數(shù))属桦。但是問題又來了熊痴,因為你返回的這個臨時對象又拿去初始化另外一個對象arr
他爸,當(dāng)然調(diào)用也是復(fù)制構(gòu)造函數(shù)。調(diào)用兩次復(fù)制構(gòu)造函數(shù)完全沒有必要果善,編譯器也會這么想诊笤,所以將其優(yōu)化:直接拿函數(shù)內(nèi)部創(chuàng)建的動態(tài)數(shù)組去初始化arr
。所以僅有一次復(fù)制構(gòu)造函數(shù)被調(diào)用巾陕,但是一旦完成arr
的創(chuàng)建讨跟,那個動態(tài)數(shù)組對象就被析構(gòu)了。最后arr
離開其作用域被析構(gòu)惜论。我們看到編譯器盡管做了優(yōu)化许赃,但是還是導(dǎo)致對象被創(chuàng)建了兩次,函數(shù)內(nèi)部創(chuàng)建的動態(tài)數(shù)組僅僅是一個中間對象馆类,用完后就被析構(gòu)了混聊,有沒有可能直接將其申請的空間直接轉(zhuǎn)移到arr
,那么資源得以重用乾巧,實際上只用申請一份內(nèi)存句喜。但是問題的關(guān)鍵是復(fù)制構(gòu)造函數(shù)執(zhí)行的是復(fù)制,不是轉(zhuǎn)移沟于,無法實現(xiàn)這樣的功能咳胃。此時第献,你需要移動構(gòu)造函數(shù):
template <typename T>
class DynamicArray
{
public:
// ...其它省略
// 移動構(gòu)造函數(shù)
DynamicArray(DynamicArray&& rhs) :
m_size{ rhs.m_size }, m_array{rhs.m_array}
{
rhs.m_size = 0;
rhs.m_array = nullptr;
cout << "Move constructor: dynamic array is moved!\n";
}
// 移動賦值操作符
DynamicArray& operator=(DynamicArray&& rhs)
{
cout << "Move assignment operator is called\n";
if (this == &rhs)
return *this;
delete[] m_array;
m_size = rhs.m_size;
m_array = rhs.m_array;
rhs.m_size = 0;
rhs.m_array = nullptr;
return *this;
}
};
上面是移動構(gòu)造函數(shù)與移動賦值操作符的實現(xiàn)询张,相比復(fù)制構(gòu)造函數(shù)與復(fù)制賦值操作符,前者沒有再分配內(nèi)存幌陕,而是實現(xiàn)內(nèi)存所有權(quán)轉(zhuǎn)移供璧。那么測試相同的代碼存崖,其結(jié)果是:
Constructor: dynamic array is created!
Move constructor: dynamic array is moved!
Destructor: dynamic array is destroyed!
Destructor: dynamic array is destroyed!
可以看到,調(diào)用的是移動構(gòu)造函數(shù)睡毒,那么函數(shù)內(nèi)部申請的動態(tài)數(shù)組直接被轉(zhuǎn)移到arr
来惧。從而減少了一份相同內(nèi)存的申請與釋放。注意析構(gòu)函數(shù)被調(diào)用兩次演顾,這是因為盡管內(nèi)部進(jìn)行了內(nèi)存轉(zhuǎn)移供搀,但是臨時對象依然存在,只不過第一次析構(gòu)函數(shù)析構(gòu)的是一個nullptr
钠至,這不會對程序有影響葛虐。其實通過這個例子,我們也可以看到棉钧,一旦你已經(jīng)自己創(chuàng)建了復(fù)制構(gòu)造函數(shù)與復(fù)制賦值運(yùn)算符后屿脐,編譯器不會創(chuàng)建默認(rèn)的移動構(gòu)造函數(shù)和移動賦值運(yùn)算符,這點要注意。最好的話摄悯,這個4個函數(shù)一旦自己實現(xiàn)一個,就應(yīng)該養(yǎng)成實現(xiàn)另外3個的習(xí)慣愧捕。
這就是移動語義奢驯,用移動而不是復(fù)制來避免無必要的資源浪費(fèi),從而提升程序的運(yùn)行效率次绘。其實在C++11
中瘪阁,STL
的容器都實現(xiàn)了移動構(gòu)造函數(shù)與移動賦值運(yùn)算符,這將大大優(yōu)化STL
容器邮偎。
std::move
移動語義前面已經(jīng)介紹了管跺,我們知道對象的移動語義的實現(xiàn)是依靠移動構(gòu)造函數(shù)和移動賦值操作符。但是前提是你傳入的必須是右值禾进,但是有時候你需要將一個左值也進(jìn)行移動語義(因為你已經(jīng)知道這個左值后面不再使用)豁跑,那么就必須提供一個機(jī)制來將左值轉(zhuǎn)化為右值。在C++
中泻云,std::move
就是專為此而生艇拍,看下面的例子:
vector<int> v1{1, 2, 3, 4};
vector<int> v2 = v1; // 此時調(diào)用復(fù)制構(gòu)造函數(shù),v2是v1的副本
vector<int> v3 = std::move(v1); // 此時調(diào)用移動構(gòu)造函數(shù)宠纯,v3與v1交換:v1為空卸夕,v3為{1, 2, 3, 4}
可以看到,我們通過std::move
將v1
轉(zhuǎn)化為右值婆瓜,從激發(fā)v3
的移動構(gòu)造函數(shù)快集,實現(xiàn)移動語義。
C++
中利用std::move
實現(xiàn)移動語義的一個典型函數(shù)是std::swap
:實現(xiàn)兩個對象的交換廉白。C++11
之前个初,std::swap
的實現(xiàn)如下:
template <typename T>
void swap(T& a, T& b)
{
T tmp{a}; // 調(diào)用復(fù)制構(gòu)造函數(shù)
a = b; // 復(fù)制賦值運(yùn)算符
b = tmp; // 復(fù)制賦值運(yùn)算符
}
從上面的實現(xiàn)可以看到:共進(jìn)行了3次復(fù)制。如果類型T
比較占內(nèi)存蒙秒,那么交換的代價是非常昂貴的勃黍。但是利用移動語義,我們可以更加高效地交換兩個對象:
template <typename T>
void swap(T& a, T& b)
{
T temp{std::move(a)}; // 調(diào)用移動構(gòu)造函數(shù)
a = std::move(b); // 調(diào)用移動賦值運(yùn)算符
b = std::move(tmp); // 調(diào)用移動賦值運(yùn)算符
}
僅通過三次移動晕讲,實現(xiàn)兩個對象的交換覆获,由于沒有復(fù)制,效率更高瓢省!
你可能會想弄息,std::move
函數(shù)內(nèi)部到底是怎么實現(xiàn)的。其實std::move
函數(shù)并不“移動”勤婚,它僅僅進(jìn)行了類型轉(zhuǎn)換摹量。下面給出一個簡化版本的std::move
:
template <typename T>
typename remove_reference<T>::type&& move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
代碼很短,但是估計很難懂。首先看一下函數(shù)的返回類型缨称,remove_reference
在頭文件<type_traits>中凝果,remove_reference<T>
有一個成員type
,是T
去除引用后的類型睦尽,所以remove_reference<T>::type&&
一定是右值引用器净,對于返回類型為右值的函數(shù)其返回值是一個右值(準(zhǔn)確地說是xvalue
)。所以当凡,知道了std::move
函數(shù)的返回值是一個右值山害。然后,我們看一下函數(shù)的參數(shù)沿量,使用的是通用引用類型(&&
)浪慌,意味者其可以接收左值,也可以接收右值朴则。其推導(dǎo)規(guī)則如下:如果實參是左值权纤,推導(dǎo)后的形參是左值引用,如果是右值乌妒,推導(dǎo)出來的是右值引用(感興趣的話可以看看reference collapsing)妖碉。但是不管怎么推導(dǎo),ReturnType
的類型一定是右值引用芥被,最后std::move
函數(shù)只是簡單地調(diào)用static_cast
將參數(shù)轉(zhuǎn)化為右值引用欧宜。所以,std::move
什么也沒有做拴魄,只是告訴編譯器將傳入的參數(shù)無條件地轉(zhuǎn)化為一個右值冗茸。所以,當(dāng)你使用std::move
作用于一個對象時匹中,你只是告訴編譯器這個對象要轉(zhuǎn)化為右值夏漱,然后就有資格進(jìn)行移動語義了!
下面舉一個由于誤用std::move
而無效的例子顶捷。假如你在設(shè)計一個標(biāo)注類挂绰,其構(gòu)造函數(shù)接收一個string
類型參數(shù)作為標(biāo)注文本,你不希望它被修改服赎,所以標(biāo)注為const葵蒂,然后將其復(fù)制給其的一個數(shù)據(jù)成員,你可能會使用移動語義:
class Annotation
{
public:
explicit Annotation(const string& text):
m_text {std::move(text)}
{ }
const string& getText() const { return m_text; }
private:
string m_text;
};
然后你高高興興地去測試:
int main()
{
string text{ "hello" };
Annotation ant{ text };
cout << ant.getText() << endl; // output: hello
cout << text << endl; // output: hello 不是空重虑,移動語義沒有實現(xiàn)
return 0;
}
你發(fā)現(xiàn)移動語義并沒有被實現(xiàn)践付,這是為什么呢?首先缺厉,從直觀上看永高,假如你移動語義成功了隧土,那么text
會發(fā)生改變,這會違反其const屬性命爬。所以曹傀,你不大可能成功!其實饲宛,std::move
函數(shù)會在推導(dǎo)形參時會保持形參的const屬性卖毁,所以其最終返回的是一個const右值引用類型,那么m_text{std::move(text)}
到底會調(diào)用什么構(gòu)造函數(shù)呢落萎?我們知道string
的內(nèi)部有兩個構(gòu)造函數(shù)可能會匹配:
class string
{
// ...
string(const string& rhs); // 復(fù)制構(gòu)造函數(shù)
string(string&& rhs); // 移動構(gòu)造函數(shù)
}
那么到底會匹配哪個呢?肯定的是移動構(gòu)造函數(shù)不會被匹配炭剪,因為不接受const對象练链,復(fù)制構(gòu)造函數(shù)會匹配嗎?答案是可以奴拦,因為前面我們講過const左值引用可以接收右值媒鼓,const右值更可以!所以错妖,你其實調(diào)用了復(fù)制構(gòu)造函數(shù)绿鸣,那么移動語義當(dāng)然無法實現(xiàn)。
所以暂氯,如果你想接下來進(jìn)行移動潮模,那不要把std::move
引用在const對象上!
std::forward與完美轉(zhuǎn)發(fā)
前面已經(jīng)講過痴施,完美轉(zhuǎn)發(fā)就是創(chuàng)建一個函數(shù)擎厢,該函數(shù)可以接收任意類型的參數(shù),然后將這些參數(shù)按原來的類型轉(zhuǎn)發(fā)給目標(biāo)函數(shù)辣吃,完美轉(zhuǎn)發(fā)的實現(xiàn)要依靠std::forward
函數(shù)动遭。下面就定義了這樣一個函數(shù):
// 目標(biāo)函數(shù)
void foo(const string& str); // 接收左值
void foo(string&& str); // 接收右值
template <typename T>
void wrapper(T&& param)
{
foo(std::forward<T>(param)); // 完美轉(zhuǎn)發(fā)
}
首先要有一點要明確,不論傳入wrapper
的參數(shù)是左值還是右值神得,一旦傳入之后厘惦,param
一定是左值,然后我們來具體分析這個函數(shù):
- 當(dāng)一個類型為
string
類型的右值傳遞給wrapper
時哩簿,T
被推導(dǎo)為string
宵蕉,param
為右值引用類型,但是一旦傳入后节榜,param
就變成了左值国裳,所以你直接轉(zhuǎn)發(fā)給foo
函數(shù),將丟失param
的右值屬性全跨,那么std::forward
就確保傳入foo
的值還是一個右值缝左; - 當(dāng)類型為
const string
的左值傳遞給wrapper
時,T
被推導(dǎo)為const string&
,param
為const左值引用類型渺杉,傳入后蛇数,param
仍為const左值類型,所以你直接轉(zhuǎn)發(fā)給foo
函數(shù)是越,沒有問題耳舅,此時應(yīng)用std::forward
函數(shù)可以看成什么也沒有做; - 當(dāng)類型為
string
的左值傳遞給wrapper
時倚评,T
被推導(dǎo)為string&
浦徊,param
為左值引用類型,傳入后天梧,param
仍為左值類型盔性,所以你直接轉(zhuǎn)發(fā)給foo
函數(shù),沒有問題呢岗,此時應(yīng)用std::forward
函數(shù)可以看成什么也沒有做冕香;
所以wrapper
函數(shù)可以實現(xiàn)完美轉(zhuǎn)發(fā),其關(guān)鍵點在于使用了std::forward
函數(shù)確保傳入的右值依然轉(zhuǎn)發(fā)為右值后豫,而對左值傳入不做處理悉尾。
那么,std::forward
到底怎么處理挫酿,其實現(xiàn)如下:
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
代碼依然與std::move
一樣簡潔构眯,我們結(jié)合wrapper
來看,如果傳入wrapper
函數(shù)中的是string
左值早龟,那么推導(dǎo)出T
是string&
鸵赖,那么將調(diào)用std::foward<string&>
,根據(jù)std::foward
的實現(xiàn)拄衰,其實例化為:
string& && forward(typename remove_reference<string&>::type& param)
{
return static_cast<string& &&>(param);
}
連續(xù)出現(xiàn)3個&
符號有點奇怪它褪,我們知道C++
不允許引用的引用,那么其實編譯器這里進(jìn)行是引用折疊(reference collapsing翘悉,大致就是后面的引用消掉)茫打,因此,變成:
string& forward(string& param)
{
return static_cast<string&>(param);
}
上面的代碼就很清晰了妖混,一個左值引用的參數(shù)老赤,然后還是返回左值引用,此時的std::foward
就是什么也沒有做制市,因為傳入與返回完全一樣抬旺。
那么如果傳入wrapper
函數(shù)中的是string
右值,那么推導(dǎo)出T
是string
祥楣,那么將調(diào)用std::foward<string>
开财,根據(jù)std::foward
的實現(xiàn)汉柒,其實例化為:
string && forward(typename remove_reference<string>::type& param)
{
return static_cast<string&&>(param);
}
繼續(xù)簡化,變成:
string&& forward(string& param)
{
return static_cast<string&&>(param);
}
參數(shù)依然是左值引用(這點是一致的责鳍,因為前面說過傳入std:;forward
中的實參一直是左值)碾褂,但是返回的是右值引用,此時的std::foward
就是將一個左值轉(zhuǎn)化了右值历葛,這樣保證傳入目標(biāo)函數(shù)的實參是右值正塌!
綜上,可以看到std::foward
函數(shù)是有條件地將傳入的參數(shù)轉(zhuǎn)化為右值恤溶,而std::move
無條件地將參數(shù)轉(zhuǎn)化為右值乓诽,這是兩者的區(qū)別。但是本質(zhì)上咒程,兩者什么沒有做鸠天,最多就是進(jìn)行了一次類型轉(zhuǎn)換。
講完了孵坚!
References
[1] cpp leraning online.
[2] Marc Gregoire. Professional C++, Third Edition, 2016.
[3] cppreference
[4] 歐長坤(歐龍崎), 高速上手 C++ 11/14.
[5] Scott Meyers. Effective Modern C++, 2014.