Introduction
不論在C或C++中,當函式要傳遞一個「傳回值」時,通常可以透過「暫存器」來傳遞,也就是:函式把將要回傳的值,暫存在某個暫存器中,接下來呼叫方就可以去讀取該暫存器,也就達到傳遞的效果,以上作法的前提是,該「暫存器」容納得下該「傳回值」;然而,在C++中,當函式的「傳回值」不再是一般小尺寸的型態,而是大尺寸的物件時,會發生什麼事?編譯器會如何來處理這種情況?本篇文章即要討論這個情況。
Return Value — 「object」
當傳回值是個C++的「物件」,且程式沒有設定傳回值最佳化時(Return Value Optimization),則需要透過兩次的物件複製來達到傳遞目的:
- 將該物件複製到堆疊上的臨時空間
- 將存放在臨時空間中的物件複製到儲存函式回傳值的物件上
然而,根據不同的compiler及不同的calling convention,甚至不同的平台,在傳遞物件上皆會有不同的實作方式。
接下來在實驗的地方,我將比較在「VC9」(WITH Windows XP)及「g++」(WITH Ubuntu 10.04 64bit)這兩種不同的compiler及不同的平台下,執行物件回傳的異同,以及Return Value Optimization的設定對於執行結果的影響。
Experiment
在實驗一跟實驗二的地方,我將在VC9及g++底下分別執行測試的程式碼,並將執行結果的截圖貼上來。而由於前面兩個實驗產生了不同的執行結果,我將在short summary的地方進一步分析原因,並透過實驗三來做驗證
#include <iostream>
using namespace std;
struct cpp_obj
{
cpp_obj()
{
cout << "ctor\n";
}
cpp_obj(const cpp_obj& c)
{
cout << "copy ctor\n";
}
cpp_obj& operator=(const cpp_obj& rhs)
{
cout << "operator=\n";
return *this;
}
~cpp_obj()
{
cout << "dtor\n";
}
};
cpp_obj return_test()
{
cpp_obj b;
cout << "before return\n";
return b;
}
int main()
{
cpp_obj n;
n = return_test();
}
實驗一是在windows XP下使用VC9,底下是執行結果的截圖
實驗二是在Ubuntu下使用g++,底下是執行結果的截圖
- short summary of Exp.1 & Exp.2
在實驗一當中,我們發現物件被複製了兩次,分別是
- 呼叫「copy constructor」來將物件複製到堆疊上的臨時空間
- 呼叫「operator=」來將存放在臨時空間中的物件複製到儲存函式回傳值的物件上
而實驗二,我們發現減少了copy constructor的呼叫,原因是因為g++ compiler在執行程式時自動做了最佳化(Return Value Optimization),將物件直接建構在臨時空間上,因此物件少了一次的複製。
因此,在實驗三,我們要做的就是將VC9的程式最佳化打開,我們可以預料得到的是: 最佳化打開後,物件勢必會減少一次的複製。
實驗三是在windows XP下使用VC9,且打開程式的最佳化(Return Value Optimization),底下是執行結果的截圖
Conclusion
本篇文章介紹了函式是如何傳遞一個C++物件,且透過實驗來說明不同的compiler會有不同的設定,根據不同的compiler及不同的calling convention,甚至不同的平台,在傳遞物件上皆會有不同的實作方式。
筆者是在VC9(WITH Windows XP)及g++(WITH Ubuntu 10.04 64bit)這兩種compiler下編譯程式的,而我們發現Return Value Optimization提升了程式執行時的效能,深深影響著實驗的結果,更多討論可以詳見Reference 2。
回傳一個物件是一件非常繁雜的工作,也因此,我們應該盡量避免回傳「物件」。
Reference
- 程式設計師的自我修養 — 連結、載入、程式庫,俞甲子、石凡、潘愛民
- MSDN, http://msdn.microsoft.com/en-us/library/ms364057%28VS.80%29.aspx