1. 程式人生 > >C++中的RVO與NVR優化

C++中的RVO與NVR優化

語義上,函式呼叫結束,返回值會通過拷貝構造一個臨時匿名物件傳出來(因為函式體中的都是區域性變數,return後的物件呼叫完成都超過作用域,不存在了)。

先上程式碼:

#include <iostream>
using namespace std;

class MyClass
{

private:
	int m_i;

public:
	MyClass(){ m_i = 0; cout << "this is default constructor!" << endl; }  //預設建構函式
	MyClass(const MyClass& that);   //拷貝建構函式
	MyClass& operator=(const MyClass& that);   //賦值建構函式
};

MyClass::MyClass(const MyClass& that)

{
	cout << "this is copy constructor!" << endl;
	this->m_i = that.m_i;
}

MyClass& MyClass::operator=(const MyClass& that)

{
	cout << "this is assignment constructor!" << endl;

	if (this != &that)
	{
		return *this;
	}
	else
	{
		this->m_i = that.m_i;
		return *this;
	}
}

MyClass gfunc()
{
	MyClass obj;
	return obj;
}

MyClass func()
{
	return MyClass();
}

int main()
{
	MyClass myObj;
	cout << "________" << endl;

	myObj = gfunc();
	cout << "________" << endl;

	MyClass myObj2 = gfunc();
	cout << "________" << endl;

	MyClass myObj3 = func();   //RVO優化
        cout << "________" << endl;

        myObj3 = func();           //RVO優化 
        cin.get();
	return 0;
}


在VS2013下,最終的輸出結果如下:


myObj = gfunc()這句,一共有3次建構函式的呼叫。包括gfunc()函式體內部的區域性物件obj的構造,return返回值一個匿名的臨時物件的構造。gfunc()函式體執行完成,return之後的obj物件已經消亡,為了把返回值傳出來,必須藉助於這個臨時物件。

藉助於C++編譯器的RVO技術(return value optimization),return 後是呼叫建構函式:若是用來給物件賦值,則會省掉一次拷貝建構函式的呼叫(用來傳出返回值的匿名臨時物件),程式碼中的 myObj3 = func();若是用來初始化物件,那麼還可以省掉一次賦值建構函式的呼叫,程式碼中的  MyClass myObj3 = func()。(注意這裡要區別C++中的初始化和賦值,初始化是分配空間的同時賦值,一個語句完成;而賦值是先前已經有了空間)。

如果return 之後,是一個具名的物件,編譯器可以做NRV優化。此時如果返回值用來初始化物件,可以省掉一次賦值建構函式的呼叫。 程式碼中的  MyClass myObj2 = gfunc() ,相當於直接用把return 後的臨時物件拷貝構造到myObj2中。

上邊兩條是編譯器預設就提供的,比如在VS下。

如果編譯器的優化能力更強,還存在更強的NVR優化技術,能省掉拷貝建構函式的呼叫。

在gfunc()函式中,內部通過預設建構函式區域性變數obj,最後又return這個區域性變數obj,對於MyClass myObj2 = gfunc() 語句,完全可以直接將myObj2 代替obj,可以在前邊的基礎上省掉了拷貝建構函式的呼叫。

myObj = gfunc() 則可以直接將return 後的結果賦值給myobj ,也省掉了拷貝建構函式的呼叫。

上邊的程式碼在GCC -O優化編譯下,輸出為:


可以看到如果return 後是一個具名變數,也就是是個左值;NRV優化可以省去拷貝構造一個臨時匿名變數。GCC下比較好開啟,VS中暫時沒找到啟用方法。

由於NRV優化的存在,可以return之後直接返回一個具名的物件。萬一不支援NRV技術,將會拷貝構造一個匿名的臨時物件,而且如果存在移動拷貝建構函式,預設的總是移動拷貝建構函式的效率更好,所以當存在移動拷貝建構函式時,將是移動拷貝構造一個臨時匿名物件,雖然此時return 後的是一個具名的左值 result,實際相當於 return move(result).

看這裡的栗子

上邊這個栗子中,函式的返回值型別不能是指向臨時變數的引用(無論左值引用還是右值引用)和指標的原則是不能變的;不過可以將函式的返回值(本身可以是個臨時變數),賦值給右值引用或者const 左值引用。注意前後的差別。因為函式呼叫結束,返回值變不存在,實際呼叫結束返回值是return 後的值的一個拷貝,return 後的那個在呼叫結束一定是不存在的。

右值引用和const左值引用,會延長臨時變數的生存週期,使得引用變數存在時,臨時變數也是存在的。