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,可以在前邊的基礎上省掉了拷貝建構函式的呼叫。
上邊的程式碼在GCC -O優化編譯下,輸出為:
可以看到如果return 後是一個具名變數,也就是是個左值;NRV優化可以省去拷貝構造一個臨時匿名變數。GCC下比較好開啟,VS中暫時沒找到啟用方法。
由於NRV優化的存在,可以return之後直接返回一個具名的物件。萬一不支援NRV技術,將會拷貝構造一個匿名的臨時物件,而且如果存在移動拷貝建構函式,預設的總是移動拷貝建構函式的效率更好,所以當存在移動拷貝建構函式時,將是移動拷貝構造一個臨時匿名物件,雖然此時return 後的是一個具名的左值 result,實際相當於 return move(result).
看這裡的栗子
上邊這個栗子中,函式的返回值型別不能是指向臨時變數的引用(無論左值引用還是右值引用)和指標的原則是不能變的;不過可以將函式的返回值(本身可以是個臨時變數),賦值給右值引用或者const 左值引用。注意前後的差別。因為函式呼叫結束,返回值變不存在,實際呼叫結束返回值是return 後的值的一個拷貝,return 後的那個在呼叫結束一定是不存在的。
右值引用和const左值引用,會延長臨時變數的生存週期,使得引用變數存在時,臨時變數也是存在的。