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::movestd::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 限定的左值參考上, 而並不是右值參考上. 除非這個變數被明確要求移動或者保持了它原來右值的屬性

大家在之後寫程式的過程之中, 一定要注意這個坑. 否則, 程式中可能會產生不必要的複製而影響程式的運作性能