为什么移动返回一个右值引用参数需要用 std::move() 包装它?

Lei*_*Hua 5 c++ c++11 c++14

我正在阅读 Effective Modern C++ Item 25, on page 172,它有一个例子来证明,如果你想移动返回一个右值引用参数,你需要用 std::move(param) 包装它。由于参数本身总是一个左值,如果没有 std::move(),它将被复制返回。

我不明白。如果 std::move(param) 只是将它接受的参数转换为右值引用,那么当 param 已经是右值引用时有什么区别?

就像下面的代码:

#include <string>
#include <iostream>
#include <utility>

template<typename T>
class TD;

class Widget {
public:
    explicit Widget(const std::string& name) : name(name) {
        std::cout << "Widget created with name: " << name << ".\n";
    }

    Widget(const Widget& w) : name(w.name) {
        std::cout << "Widget " << name << " just got copied.\n";
    }

    Widget(Widget&& w) : name(std::move(w.name)) {
        std::cout << "Widget " << name << " just got moved.\n";
    }

private:
    std::string name;
};

Widget passThroughMove(Widget&& w) {
    // TD<decltype(w)> wType;
    // TD<decltype(std::move(w))> mwType;
    return std::move(w);
}

Widget passThrough(Widget&& w) {
    return w;
}

int main() {
    Widget w1("w1");
    Widget w2("w2");

    Widget wt1 = passThroughMove(std::move(w1));
    Widget wt2 = passThrough(std::move(w2));

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

它输出:

Widget created with name: w1.
Widget created with name: w2.
Widget w1 just got moved.
Widget w2 just got copied.
Run Code Online (Sandbox Code Playgroud)

在 passThroughMove(Widget&& w) 中,w 的类型已经是右值引用,std::move(w) 只是再次将其转换为右值引用。如果我取消注释 TD 行,我可以看到 decltype(w) 和 decltype(std::move(w)) 都是 Widget &&:

move_parameter.cpp:27:21: error: implicit instantiation of undefined template 'TD<Widget &&>'
    TD<decltype(w)> wType;
                    ^
move_parameter.cpp:28:32: error: implicit instantiation of undefined template 'TD<Widget &&>'
    TD<decltype(std::move(w))> mwType;
                               ^
Run Code Online (Sandbox Code Playgroud)

由于 w 和 std::move(w) 都是相同的右值引用类型,为什么“return std::move(w)”移动 w,而“return w”只复制?

编辑:感谢您的回答和评论。我现在有了更好的理解,但不确定它是否准确。所以 std::move(w) 返回一个右值引用,就像 w 本身一样。但是 std::move(w) 作为一个函数调用,它本身就是一个右值,所以它可以被移动。w作为一个命名变量,它本身是一个左值,虽然它的类型是右值引用,所以它不能被移动。

Yak*_*ont 5

表达式的类型不同于变量的类型,decltype两者兼而有之。

decltype(w)
Run Code Online (Sandbox Code Playgroud)

是变量 w。

decltype((w))
Run Code Online (Sandbox Code Playgroud)

是表达式的类型w(好吧,(w)但它们是相同的)。

如果你有一个 type 变量foo&&,当在表达式中使用时,它的类型是foo&——它被命名,因此是一个左值。

这有点道理。 foo&&只是意味着它可以绑定到一个临时的。一旦绑定,它就有了一个名字,可以多次使用。

任何可以多次使用的东西都不应该被隐式地移出。

命名事物是左值的这条规则的唯一例外是返回规则的隐式移动。在少数情况下,会发生省略但由于某种原因被阻止的情况下,值会被隐式移动。这些例外不适用于此处。

  • 请注意,从 [P1825R0](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1825r0.html) 开始,并根据 [\[class.copy. elision\]/3](https://timsong-cpp.github.io/cppwp/class.copy.elision#3),这个特殊的例子(`passThrough(Widget&amp;&amp; w)`)也成为了一个例外,并且左值“w”,在重载决策中,为“return”语句选择构造函数时,可以被视为_就好像它是右值_,从而选择移动构造函数而不是复制构造函数。 (3认同)

Eme*_*pon 1

在 passThroughMove(Widget&& w) 中,w 的类型已经是右值引用,std::move(w) 只是再次将其转换为右值引用。

所以 std::move(w) 返回一个右值引用,就像 w 本身一样。

不,std::move(w)转换为右值,而右值引用是左值。

两者都按值运行passThroughMovepassThrough返回。然而,它们在内部创建此类返回值的方式上有所不同。在内部,passThroughMove通过移动创建其返回值。通过移入新对象(返回值)来创建它,这就是对返回值Widget的影响。另一方面通过复制创建自己的返回值。std::movepassThrough

事实是,该任务

Widget wt2 = passThrough(std::move(w2));
Run Code Online (Sandbox Code Playgroud)

从右值完成不会改变passThrough强制通过复制创建其返回值的事实。

在代码的输出中,您可以看到上述语义加上 RVO 的效果。如果没有 RVO,这两个分配都会导致两个额外的移动结构,并被优化掉。

  • &gt; std::move(w) 转换为右值,而右值引用是左值。这是金子。 (2认同)