摘要訊息 : 右值參考也是左值?
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
.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :