在 C++ 11 中, 引入了移動語意, 從而延伸了 C++ 中的右值 - 右值參考因此誕生
這幾天在重構資料結構的程式碼的時候, 發現了 C++ 中關於右值移動的一個坑
我們通過程式碼來分析這個坑 :
#include <iostream>
using namespace std;
class Foo {
public:
Foo() = default;
Foo(const Foo &) {
cout << "copy constructor called" << endl;
}
Foo(Foo &&) noexcept {
cout << "move constructor called" << endl;
}
~Foo() = default;
};
void f(const Foo &value) {
cout << "const Foo &" << endl;
auto x {value};
}
void f(Foo &&value) {
cout << "Foo &&" << endl;
auto x {value};
}
int main(int argc, char *argv[]) {
Foo a;
f(a);
f(move(a));
}
這一段程式碼最終的輸出結果是什麼呢?
你可能會這麼想 :
宣告一個 Foo 型別的 a, 使用的是 Foo 的預設建構子, 那麼這個過程不會有任何輸出. 接下來呼叫 f 函式, 並且放入之前宣告的 a. 這個 a 屬於左值, 所以顯然呼叫的是
void f(const Foo &);
函式中, 又宣告了一個 x, 以 a 為其初始值. 這個過程中, 除了輸出 "const Foo &" 之外, 還呼叫 Foo 的複製建構子也會輸出 "copy constructor called". 最後, 通過移動 a 的方式, 呼叫 f 函式. 很明顯函式中的輸出應該是 "Foo &&". 接下來又宣告了一個 x, 以 a 為初始值. 因為這個時候 a 為右值參考, 所以直接對 a 進行移動, 此時應該輸出 "move constructor called". 即最終的輸出是const Foo & copy constructor called Foo && move constructor called
這樣一看好像沒什麼問題, 但實際上是錯的
這段程式碼最終的輸出應該是
const Foo & copy constructor called Foo && copy constructor called
前面的沒什麼問題, 但是最後為什麼還是呼叫複製建構子而不是呼叫移動建構子呢?
在《C++ 學習筆記》中, 我們曾經提到 : 所有的變數都是左值, 不管對他初始化的值是不是右值
你可能還是沒有明白這裡有什麼關係
回頭看
void f(Foo &&value) {
cout << "Foo &&" << endl;
auto x {value};
}
再看看我們曾經提過的內容, 你就會突然明白, 這裡 value 是個左值而不是右值, 儘管它是右值參考型別, 但是這不影響 value 是個左值這樣一個事實. 所以, 一個左值自然而然地就會被繫結到複製建構子上, 而不是移動建構子. 因此, 這裡多了一次複製, 而且是不必要的複製. 因為這個 value 我們原本就假設用完即棄
那如何讓其進行移動而不是複製呢
很簡單, 我們之前也提過標準程式庫函式 std::move
和 std::forward
使用 std::move
這裡非常好理解, 就是讓 value 這個左值被強行移動
而使用 std::forward
, 則是為了保持 value 原來是個右值的屬性, 避免 value 被當作左值而產生不必要的複製
我們重作 void f(Foo &&) 函式 :
void f(Foo &&value) {
cout << "Foo &&" << endl;
auto x1 {std::move(value)};
auto x2 {std::forward<Foo>(value)};
auto x3 {static_cast<Foo &&>(value)};
}
此時, 程式碼最終的輸出結果為
const Foo & copy constructor called Foo && move constructor called move constructor called move constructor called
這次全部變成了移動, 而不是複製
你可能對 x3 變數會產生疑惑. 這也是我們之前在《C++ 學習筆記》中提過的, 一個左值雖然不能被隱含地轉型為右值, 但是可以通過明確型別轉換來明確地被轉型為右值. 這裡通過 static_cast<Foo &&>
讓 value 從左值變成了右值, 從而與移動建構子進行匹配. 而事實上, 這也是 std::move
所幹的事情
因此, 我們可以總結出 : 右值參考並不意味著一個物件會被移動, 因為一個右值參考型別的變數也是一個左值, 它會被自動繫結到被 const
限定的左值參考上, 而並不是右值參考上. 除非這個變數被明確要求移動或者保持了它原來右值的屬性
大家在之後寫程式的過程之中, 一定要注意這個坑. 否則, 程式中可能會產生不必要的複製而影響程式的運作性能
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :