什麼是左值,什麼是右值?
左值就是程序能獲得其地址的表示數據的表達式,包括變量,const常量,解除引用的指針。
相反,右值就是不能應用地址運算符&的表示數據的表達式,包括字面常量,x+y,非引用的返回值。
什麼是左值引用,什麼是右值引用?
我們常說的C++的引用,大部分時候指的就是左值引用,符號是&,
比如 int a=10;int &b=a; 其中,b就是a的引用,可以理解爲別名。
右值引用符號是&&,
比如 int &&a = 10; 其中,a就是10的右值引用。&10是非法的,但是&a卻是合法的。
移動語義和右值引用的關係
移動語義對降低C++構造和析構的開銷有重要的意義,減少了傳值、返回值過程中的資源拷貝。
C++移動語義的實現,正是基於右值引用。
傳值和返回值的代碼開銷在哪裏?
請看下面這段代碼;
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
struct Person {
Person(const char* p) {
cout << "constructor" << endl;
}
Person(const Person& p) {
cout << "copy constructor" << endl;
}
const Person& operator=(const Person& p) {
cout << "operator=" << endl;
return *this;
}
~Person() {
cout << "destructor" << endl;
}
};
Person getAlice() {
Person p("alice"); // 對象創建。調用構造函數,一次 new 操作
return p; // 返回值創建。調用拷貝構造函數,一次 new 操作
// p 析構。一次 delete 操作
}
int main() {
cout << "______________________" << endl;
Person a = getAlice(); // 對象創建。調用拷貝構造函數,一次 new 操作
// 返回值析構,一次 delete 操作
// 當前步驟合共 3次構造,2次析構
cout << "______________________" << endl;
a = getAlice(); // 對象創建。調用拷貝構造函數,一次 new 操作
// 返回值析構,一次 delete 操作
// 當前步驟合共 3次構造,2次析構
cout << "______________________" << endl;
return 0;
// a 析構。一次 delete 操作
}
在不考慮NVRO(返回值優化)的情況下,上面這段代碼的預期過程如註釋,總共6次構造,5次析構。
當然了,編譯器會進行NVRO(返回值優化),減少構造和析構次數。
不同編譯器的NVRO結果是不一樣的:
在Visual Studio 2015上面編譯運行結果是:
Person a = getAlice(),這一步,getAlice裏面p的析構和返回值的構造被優化掉了,相當於a直接用了getAlice()的對象;
a=getAlice(),這一步,沒有NVRO優化。
g++(8.2.0)優化程度比VS高。
Person a = getAlice(),這一步,getAlice() 裏面p的析構,返回值的構造和析構,a的拷貝構造都被優化掉了;
a=getAlice(),這一步,NVRO優化程度比賦初值操作的低,
getAlice() 裏面p的析構和返回值的構造被優化掉了,相當於a直接用了getAlice裏面的對象;
上面的代碼還能優化嗎?
可以。
通過移動語義,可以把拷貝構造函數改寫成移動構造函數;或者就是另外寫一個移動構造函數,實現重載。參考[4]
使用std::move相當於顯式使用移動語義。std::move()實際上是static_cast<T&&>()的簡單封裝。
用右值引用實現移動語義,從而優化拷貝構造函數
參見以下代碼和註釋
// 基於左值引用的拷貝構造函數
//(參數p設置const屬性,不允許直接取用參數p的指針成員,這是爲了拷貝構造函數既能接受左值參數,也能接受右值參數)
//(不設置const屬性也行,但是就不能用右值(getAlice的返回值)進行拷貝構造得到新的對象了。)
const Person& operator=(const Person& p) {
cout << "operator=" << endl;
delete[] name;
int len = strlen(p.name) + 1;
name = new char[len];
memcpy(name, p.name, len); //左值引用的拷貝構造,會有一次申請內存和數據拷貝
return *this;
}
// 基於右值引用的拷貝構造函數
//(不需要const了,那麼就可以直接取用參數p的指針成員,且可以在取用後將p的指針成員置爲nullptr,這樣該塊內存就不會被析構了)
const Person& operator=(Person&& p) {
cout << "operator=" << endl;
delete[] name;
name = p.name; //直接取用
p.name = nullptr; //置空使得系統無法將該塊內存析構掉
//相對比左值引用,右值引用的拷貝構造可以實現更加高效:少了一次內存申請和拷貝。
return *this;
}
【參考】
[1]《C++ Primer Plus》,18.1.9,右值引用一節
[2] https://harttle.land/2015/10/11/cpp11-rvalue.html 這篇文章講的比較易於理解
[3] 如何評價 C++11 的右值引用(Rvalue reference)特性? - Tinro的回答 - 知乎 https://www.zhihu.com/question/22111546/answer/30801982