C++ 11 右值引用和移動語義的實現

什麼是左值,什麼是右值?

左值就是程序能獲得其地址的表示數據的表達式,包括變量,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

[4] https://www.cnblogs.com/dongdongweiwu/p/4743661.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章