通常來說,當方法返回對象的一個實例時,會創建一個臨時對象,並通過複製構造函數複製到目標對象中。在C++標準中,允許省略複製構造函數(哪怕會導致不同的程序行爲),但這有一個副作用,就是編譯器可能會把兩個對象當成一個。Visual C++ 8.0(Visual C++ 2005)充分利用了C++標準的可伸縮性,加入了一些新的特性——命名返回值優化(NRVO)。NRVO消除了基於堆棧返回值的複製構造函數和析構函數,並去除了對多餘複製構造函數和析構函數的調用,從而全面地提高了程序的性能。但要注意到,優化和未優化的代碼,可能會有不同的程序行爲表現。
而在某些情況下,NRVO不會進行優化(參見優化的侷限性一節),以下是一些常見的情況:
·不同的路徑返回不同的命名對象
·引入EH狀態的多重返回路徑(甚至所有的路徑中返回相同的命名對象)
·由內聯彙編語句引用的命名返回對象
NRVO優化概述
以下是一個簡單的示例,演示了優化是怎樣被實現的:
A MyMethod (B &var) { A retVal; retVal.member = var.value + bar(var); return retVal; } |
使用上述函數的程序可能會有一個像如下所示的構造函數:
valA = MyMethod(valB); |
由MyMethod返回的值會創建在內存空間中,並通過隱藏的參數指向ValA。以下是函數中帶有隱藏參數,並清晰地寫明構造和析構函數時樣子:
A MyMethod (A &_hiddenArg, B &var) { A retVal; retVal.A::A(); // retVal的構造函數 retVal.member = var.value + bar(var); _hiddenArg.A::A(retVal); // A的複製構造函數 return; retVal.A::~A(); // retVal的析構函數 } |
從以上代碼中,很明顯可看出有一些可以優化的地方。最基本的想法是消除基於堆棧的臨時值(retVal),並使用隱藏參數,從而消除那些基於堆棧值的複製構造函數和析構函數。以下是NRVO優化過的代碼:
A MyMethod(A &_hiddenArg, B &var) { _hiddenArg.A::A(); _hiddenArg.member = var.value + bar(var); Return } |
示例代碼
Sample1.cpp:比較簡單的示例代碼
#include <stdio.h> class RVO { public: RVO(){printf("I am in constructor\n");} RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");} ~RVO(){printf ("I am in destructor\n");} int mem_var; }; RVO MyMethod (int i) { RVO rvo; rvo.mem_var = i; return (rvo); } int main() { RVO rvo; rvo=MyMethod(5); } |
打開或關閉NRVO編譯sample1.cpp,將會產生不同的程序行爲。
不帶NRVO編譯(cl /Od sample1.cpp),下面是輸出內容:
I am in constructor I am in constructor I am in copy constructor I am in destructor I am in destructor I am in destructor |
用NRVO選項編譯(cl /O2 sample1.cpp),以下是輸出內容:
I am in constructor I am in constructor I am in destructor I am in destructor |
Sample2.cpp:較複雜一點的代碼
#include <stdio.h> class A { public: A() {printf ("A: I am in constructor\n");i = 1;} ~A() { printf ("A: I am in destructor\n"); i = 0;} A(const A& a) {printf ("A: I am in copy constructor\n"); i = a.i;} int i, x, w; }; class B { public: A a; B() { printf ("B: I am in constructor\n");} ~B() { printf ("B: I am in destructor\n");} B(const B& b) { printf ("B: I am in copy constructor\n");} }; A MyMethod() { B* b = new B(); A a = b->a; delete b; return (a); } int main() { A a; a = MyMethod(); } |
不帶NRVO(cl /Od sample2.cpp)的輸出如下:
A: I am in constructor A: I am in constructor B: I am in constructor A: I am in copy constructor B: I am in destructor A: I am in destructor A: I am in copy constructor A: I am in destructor A: I am in destructor A: I am in destructor |
當打開NRVO優化時,輸出如下:
A: I am in constructor A: I am in constructor B: I am in constructor A: I am in copy constructor B: I am in destructor A: I am in destructor A: I am in destructor A: I am in destructor |
在某些情況下,NRVO優化不會起作用,以下是存在優化侷限性的一些示例程序。
Sample3.cpp:含有例外的代碼
在例外(Exception)情況中,隱藏參數必須在它被替換的臨時範圍內析構。
//RVO類定義在sample1.cpp中 #include <stdio.h> RVO MyMethod (int i) { RVO rvo; rvo.mem_var = i; throw "I am throwing an exception!"; return (rvo); } int main() { RVO rvo; try { rvo=MyMethod(5); } catch (char* str) { printf ("I caught the exception\n"); } } |
不帶NRVO編譯(cl /Od /EHsc sample3.cpp),輸出如下:
I am in constructor I am in constructor I am in destructor I caught the exception I am in destructor |
如果註釋掉“throw”語句,輸出將會如下如示:
I am in constructor I am in constructor I am in copy constructor I am in destructor I am in destructor I am in destructor |
現在,如果註釋掉“throw”語句,並且用NRVO編譯,程序輸出如下:
I am in constructor I am in constructor I am in destructor I am in destructor |
這就是說,不管打開或關閉NRVO選項,sample3.cpp的程序行爲都一樣。
Sample4.cpp:不同的命名對象
想要充分利用優化,所有的返回路徑必須都返回相同的命名對象,請看如下示例代碼:
#include <stdio.h> class RVO { public: RVO(){printf("I am in constructor\n");} RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");} int mem_var; }; RVO MyMethod (int i) { RVO rvo; rvo.mem_var = i; if (rvo.mem_var == 10) return (RVO()); return (rvo); } int main() { RVO rvo; rvo=MyMethod(5); } |
優化打開時(cl /O2 sample4.cpp)的輸出,與沒有進行任何優化時(cl /Od sample.cpp)的輸出是一樣的。因爲不是所有的返回路徑都返回同一命名對象,所以NRVO此時不起任何作用。
I am in constructor I am in constructor I am in copy constructor |
如果把上述代碼的所有返回路徑都改爲返回rvo(如下例Sample4_modified.cpp),此時優化就會消除多餘的複製構造函數。
經過修改的Sample4_Modified.cpp,以利用NRVO。
#include <stdio.h> class RVO { public: RVO(){printf("I am in constructor\n");} RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");} int mem_var; }; RVO MyMethod (int i) { RVO rvo; if (i==10) return (rvo); rvo.mem_var = i; return (rvo); } int main() { RVO rvo; rvo=MyMethod(5); } |
此時(cl /O2 Sample4_Modified.cpp)的輸出:
I am in constructor I am in constructor |
Sample5.cpp:EH限制
以下的Sample5與Sample4基本一致,除了增加了RVO類的析構函數,並具有多重返回路徑,且引入的析構函數在函數中創建了一個EH狀態。由於編譯器跟蹤的複雜性,此類對象通常需要被析構,但它阻止了返回值優化,這也是Visual C++ 2005在將來需要改進的地方。
//RVO類定義在sample1.cpp中 #include <stdio.h> RVO MyMethod (int i) { RVO rvo; if (i==10) return (rvo); rvo.mem_var = i; return (rvo); } int main() { RVO rvo; rvo=MyMethod(5); } |
不論打開或關閉優化,Sample5.cpp都會產生相同的結果。
I am in constructor I am in constructor I am in copy constructor I am in destructor I am in destructor I am in destructor |
要想打開NRVO優化,必須消除多重返回點,可以像如下所示修改MyMethod:
RVO MyMethod (int i) { RVO rvo; if (i!=10) rvo.mem_var = i; return(rvo); } |
Sample6.cpp:內聯彙編限制
當命名返回對象被內聯彙編語句所引用時,編譯器不會進行NRVO優化,請看下例代碼:
#include <stdio.h> // RVO類定義在sample1.cpp中 RVO MyMethod (int i) { RVO rvo; __asm { mov eax,rvo //可以註釋掉此行 mov rvo,eax //可以註釋掉此行 } return (rvo); } int main() { RVO rvo; rvo=MyMethod(5); } |
即使打開優化選項(cl /O2 sample6.cpp)來編譯sample6.cpp,NRVO也不會起作用。這是因爲內聯彙編語句中引用了返回對象,因此,打開或關閉優化選項,輸出都會像如下所示:
I am in constructor I am in constructor I am in copy constructor I am in destructor I am in destructor I am in destructor |
從以上輸出,可清楚地看到,複製構造函數和析構函數並沒有被消除。但如果註釋掉彙編語句,優化將會消除掉這些函數調用。
優化的副作用
程序員必須意識到,如此之類的優化將會影響到程序的流程,以下的示例代碼演示了優化所帶來的影響。
Sample7.cpp
#include <stdio.h> int NumConsCalls=0; int NumCpyConsCalls=0; class RVO { public: RVO(){NumConsCalls++;} RVO (const RVO& c_RVO) {NumCpyConsCalls++;} }; RVO MyMethod () { RVO rvo; return (rvo); } void main() { RVO rvo; rvo=MyMethod(); int Division = NumConsCalls / NumCpyConsCalls; printf ("Constructor calls / Copy constructor calls = %d\n",Division); } |
不帶優化選項編譯sample7.cpp(cl /Od sample7.cpp),程序的輸出是在意料之中的,構造函數被調用了兩次,而複製構造函數被調用了一次,因此,輸出的結果爲2。
Constructor calls / Copy constructor calls = 2
另一方面,如果打開優化選項編譯上述代碼(cl /O2 sample7.cpp),NRVO將會起作用,並消除掉複製構造函數,因此NumCpyConsCalls結果爲零,從而引發程序的除零錯誤。如果像sample7.cpp中那樣沒有很好地處理這種例外錯誤,將會導致程序崩潰。
從以上可看出,命名返回值優化(NRVO)消除了多餘的函數調用,從而在一定程度上提高了程序的速度,需記住的是,優化有時也會有副作用,請謹慎使用。