返回值和右值引用的傳遞問題
阿新 • • 發佈:2019-01-09
最近突然發現了這個問題,挺有意思的,記錄下來備忘。
以下程式碼在gcc 4.8.1下編譯測試。
測試類
測試類結構如下:
class Test2 { public: Test2() {} Test2(const char* str); Test2(const Test2& o); Test2(Test2&& o); virtual ~Test2(); Test2& operator=(const Test2& o); Test2& operator=(Test2&& o); void swap(Test2& o); const char* cstr() const { return _blocks ? _blocks : ""; } protected: char* _blocks; // 儲存字串的緩衝區 };
可以看到,這個類中包含了C++11標準中規定的若干元素:
- 預設建構函式(可預設);
- 引數建構函式(可預設);
- 解構函式;
- copy建構函式;
- move建構函式(轉移建構函式);
- copy賦值運算子;
- move賦值運算子(轉移賦值運算);
- 物件交換函式;
突然想了解一下具有move建構函式和move賦值運算的類,在物件傳遞時會發生什麼情況,所以寫了下面的幾個函式進行測試。/** * 引數構造器 * @param [in] str 字串值 */ Test2::Test2(const char* str) : _blocks(NULL) { if (str) _blocks = ::strdup(str); } /** * 拷貝建構函式 * @param [in] o 同類型的另一個物件引用 */ Test2::Test2(const Test2& o) : _blocks(NULL) { if (o._blocks) _blocks = ::strdup(o._blocks); } /** * Move建構函式 * @param [in] o 同類型的另一個物件右值引用 */ Test2::Test2(Test2&& o) : _blocks(NULL) { swap(o); } /** * 解構函式 */ Test2::~Test2() { if (_blocks) ::free(_blocks); _blocks = NULL; } /** * 賦值運算子過載 * @param [in] o 同類型的另一個物件引用 * @return 當前型別的另一個引用 */ Test2& Test2::operator=(const Test2& o) { if (this != &o) Test2(o, int()).swap(*this); return *this; } /** * 右值引用賦值運算子過載 * @param [in] o 同類型的另一個物件右值引用 * @return 當前型別的另一個引用 */ Test2& Test2::operator=(Test2&& o) { if (this != &o) { swap(o); o.~Test2(); } return *this; } /** * 交換兩個物件 * @param [in] o 同類型的另一個物件 */ void Test2::swap(Test2& o) { std::swap(_blocks, o._blocks); }
第一個函式,返回函式內部產生的區域性變數:
/**
* 測試返回內部具備變數
* @return 返回臨時生成的物件
*/
Test2 return_object()
{
Test2 res = "test";
return res;
}
通過如下程式碼測試
Test2 t1 = return_object();
t1 = return_object();
結論:
- 第一行程式碼中,只在呼叫函式內部執行了一次引數建構函式(構造區域性物件),沒有發生copy建構函式(或者move建構函式)的呼叫
- 第二行程式碼執行時,變數t1已經被初始化,所以賦值運算是必然會發生的,此時除過在呼叫函式內部執行了一次引數建構函式(構造區域性物件)外,還執行了一次move賦值運算,可見編譯器認為函式的返回值是右值。由於有了move賦值運算子,所以沒有呼叫copy賦值運算子,相當於將函式內部的區域性物件(右值)轉移到了t1變數(左值)中,完成了右到左的轉化(減少了一次構造和析構);
第二個函式,返回函式內部產生的區域性變數的引用:
/**
* 測試返回區域性變數的引用
* @return 返回臨時生成的物件的引用
*/
Test2& return_reference()
{
Test2 res = "test";
return res;
}
這個函式一看就是錯誤的,返回區域性變數的引用或指標都是不允許的,因為在函式返回前,區域性變數就會被析構,導致返回的引用是無效引用(已經遊離),為了測試的完整性,用如下程式碼測試:
Test2 t2 = return_reference();
t2 = return_reference();
結論:
- 第一行程式碼中,在呼叫函式內部執行了引數建構函式構造了局部物件,之後又執行了copy建構函式,其含義是將返回的區域性物件引用,通過copy建構函式來構造變數t2物件,但結果是變數t2不一定可以構造成功,即使構造成功了其值也不正確,顯然在呼叫copy建構函式的時候,區域性物件已經析構,copy的值無效;
- 第二行程式碼中,在呼叫函式內部執行了引數建構函式構造了局部物件,之後又執行了copy賦值函式,結果和第一行程式碼類似;
第三個函式,返回函式內部產生區域性變數的右值引用:
/**
* 測試返回區域性變數的右值引用
* @return 返回臨時生成的物件的右值引用
*/
Test2 return_right_reference()
{
Test2 res = "test";
return std::move(res);// move函式在這裡的作用是將res的引用型別轉換為右值引用型別
}
以如下程式碼進行測試:
Test2 t3 = return_right_reference();
t3 = return_right_reference();
結論:
- 第一行程式碼中,除了呼叫引數建構函式構造區域性物件外,還呼叫了一次move建構函式,這是由於返回值變成了區域性物件的右值引用,和變數t3型別不同,所以又額外的呼叫了一次move建構函式對變數t3進行初始化;
- 第二行程式碼中,情況就比較複雜了。照例通過引數建構函式構造了局部物件,但返回的是其右值引用,所以又呼叫了一次move建構函式,通過該右值引用產生了一個臨時的Test2物件(右值物件),最後通過一個move賦值運算將臨時的Test2物件轉移給變數t3;
第四個函式,對第三個函式進行修改:
/**
* 測試返回區域性變數的右值引用
* @return 返回臨時生成的物件的右值引用
*/
Test2&& return_right_reference2()
{
Test2 res = "test";
return std::move(res); // move函式在這裡的作用是將res的引用型別轉換為右值引用型別
}
結論:
這段程式碼執行的結果和“第二個函式”一樣,返回區域性變數的引用(不管是左值還是右值)都不會有正確結果。
總結:
最後發現,最樸素的寫法反而是執行效率最高的寫法(“第一個函式”),這種寫法充分的利用了編譯器在構造物件時進行的優化以及move賦值運算帶來的優勢,避免了物件在傳遞過程中產生的臨時物件以及引發的構造和析構;這也體現了move賦值運算存在的必要性。無論如何,都不能在函式內部返回臨時變數的指標或引用,無論該引用是左值引用還是右值引用。C++11也從來沒有認為變數的控制權被轉移後析構就不再發生了。所以要在函式內部產生一個物件並返回,正確的做法是:1)將物件建立在堆記憶體上並返回地址;2)返回區域性物件,並通過copy複製運算子在函式外複製該區域性物件的副本;3)返回區域性物件(是一個右值),並通過move複製運算子將返回的區域性物件轉移到另一個物件中;
move函式不能亂用,C++在一些場合下,隱含著右值的概念(比如函式返回值就是右值),此時將值進行型別轉換都會導致額外的不必要開銷(例如將返回值必須是“右值”,如果將其轉為“右值引用”,編譯器仍要生成程式碼將其轉回“右值”的物件,等於做了一堆無用功)。
上面這些結論在C++文件裡說的很明白,但以前也從沒有專門思考過,這次做一個測試,發現了一些沒有發現的問題,特別是move賦值運算在傳遞返回值時的作用和move函式在返回時的無效性。所以有些東西光看文件是不夠的,還得親手試一下。