摘要訊息 : 右值參考也是左值?

0. 前言

C++ 11 中, 引入了移動語意, 從而延伸了 C++ 中的右值, 右值參考自此誕生. 但是移動中有一些可能令人混淆的概念.

#include <iostream>
#include <utility>

class Foo {
public:
    Foo() = default;
    Foo(const Foo &) noexcept {
        std::cout << "copy constructor called" << std::endl;
    }
    Foo(Foo &&) noexcept {
        std::cout << "move constructor called" << std::endl;
    }
    ~Foo() = default;
};

void f(const Foo &value) noexcept {
    std::cout << "const Foo &" << std::endl;
    auto x {value};
}

void f(Foo &&value) noexcept {
    std::cout << "Foo &&" << std::endl;
    auto x {value};
}

int main(int argc, char *argv[]) {
    Foo a;
    f(a);
    f(std::move(a));
}

在接著往下面看之前, 你可以自己嘗試分析一下這段程式碼的輸出應該是什麼.

更新紀錄 :

  • 2022 年 4 月 8 日進行第一次更新和修正.

1. 一個猜測

對於 Code 1 的結果, 你可能會這麼想 : 宣告一個 Foo 型別的 a, 呼叫的是 Foo 的預設建構子, 那麼這個過程不會有任何輸出. 接下來呼叫 f 函式, 並且放入之前宣告的 a. 這個 a 屬於左值, 所以顯然呼叫的是 void f(const Foo &) noexcept. 函式中, 又宣告了一個 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

前面三個輸出的分析是正確的, 最後一個分析是錯誤的.

2. 正確答案

正確的輸出應該是

const Foo &
copy constructor called
Foo &&
copy constructor called

也就是最後一個輸出和第 1 節中的猜測結果不同. 那最後為什麼還是呼叫複製建構子而不是呼叫移動建構子呢? 那是因為函式 void f(Foo &&value) noexcept 中, value 是一個左值, 而不是一個右值. 自然地, value 作為左值進行複製的時候, 自然不會呼叫 Foo(Foo &&) noexcept, 而是呼叫複製建構子.

3. 符合預期

那麼如何讓最終的輸出結果和第 1 節中一樣呢? 也就是最後一個結果是輸出 move constructor called, 而不是 copy constructor called.

很顯然, 我們應該借助 std::move. 我們修改 void f(Foo &&value) noexcept

void f(Foo &&value) noexcept {
    std::cout << "Foo &&" << std::endl;
    auto x {std::move(value)};
}

就可以了.

另外, 把 std::move 替換為 std::forward 也可以得到預期結果. 因為 std::forward 在此處的含義是保持 value 型別的右值屬性, 而 std::move 則是強行讓 value 變成一個右值. 因此在此處, 使用 std::forward 和使用 std::move 的意義是相同的.

4. 總結

在程式設計中, 我們一定要注意型別為右值參考的變數是左值這個概念, 如果要保持移動的話, 就需要借助 std::move 或者 std::forward.