将对象作为函数参数时的移动语义

11

我正在研究移动构造函数和移动赋值操作,并遇到了这个问题。首先是代码:

#include <iostream>
#include <utility>

class Foo {
    public:
        Foo() {}

        Foo(Foo&& other) {
            value = std::move(other.value);
            other.value = 1; //since it's int!
        }

        int value;

    private:
        Foo(const Foo& other);
};


void Bar(Foo&& x) {
    std::cout << "# " << x.value << std::endl;
}

int main() {
    Foo foo;
    foo.value = 5;

    Bar(std::move(foo));
    std::cout << foo.value << std::endl;

    return 0;
}

依我之见,当我使用:

Bar(std::move(foo));

在Bar函数中,程序应该使用移动构造函数将foo对象“移动”到创建的临时对象temp中。这样做会使foo对象的值等于零。不幸的是,似乎Bar函数中作为参数的对象是某种引用,因为它没有“移动”原始值,但使用Bar的参数可以更改它。

请问有人能向我解释一下为什么我在控制台上看到:

#5
5

而不是

#5
0 //this should be done by move constructor?

1
请看这里:内置类型是否具有移动语义? - davidhigh
4
如果你改为使用 void Bar(Foo x);,进行比较可能会更有教育意义。请注意保持内容准确无误,同时使其易于理解。 - aschepler
1
如果函数是 Bar(Foo&& x),那么在 Bar 函数内部,它 完全相同 于函数是 Bar(Foo& x)。唯一的区别在于,在调用函数时可以绑定哪些类型的对象/引用。 - M.M
@davidhigh:是的,我已经看到了,如果你仔细阅读,你会知道它不是关于那个的...看看“value = std::move(other.value); other.value = 1; //since it's int!”代码... - RippeR
@MattMcNabb - 谢谢,这正是我想要的。我没有意识到 && 像普通引用一样工作(好吧,不完全是...),并且认为它应该构造对象(如果我现在考虑一下,这相当愚蠢)。 - RippeR
@RippeR:你说得对,我回答得有点快了。请看我的回答。 - davidhigh
4个回答

25

一个右值引用实际上是(惊喜:)一个引用。

您可以从中移动,但 std::move 不会移动。

所以,如果您不从其中移动,实际上将在 rvalue 对象上操作(通过 rvalue 引用)。


通常的模式是

void foo(X&& x)
{
    X mine(std::move(x));

    // x will not be affected anymore
}

然而,当你这样做时

void foo(X&& x)
{
    x.stuff();
    x.setBooDitty(42);
}

实际上,X&&只是作为传统引用来起作用。


谢谢,第二段代码解释了我没有意识到的问题。基于你的第一段代码,请参考这个链接:http://ideone.com/3BmMzm 为什么第一行的foo()函数不起作用?第二行和第三行使用相同的类型(如后面的行所示)。 - RippeR
你没有定义一个复制构造函数,所以你不能复制构造一个Bar(_添加Bar(Bar const&) = default;来修复它_)。(Bar&&)bar只是一种(非常)不安全的编写std::move(bar)的方式。 - sehe
1
因为你只能从rvalue引用中移动...哦,对了,Bar&& value参数是一个_lvalue_,所以你需要再次转换为rvalue(使用... std::move)。C++是一门有趣的语言。你会掌握它的。 - sehe
是的,这也是引用折叠。但仍然超出了评论线程的范围。Bar&& value 是一个常量左值,它引用了一个我们之后不再关心的 Bar 对象(因此我们承诺编译器可以将其视为 rvalue 引用)。 - sehe
@sehe,是的,我意识到了并删除了 :) 感谢您的解释! - vsoftco
显示剩余3条评论

9
当你写下value = std::move(other.value);时,你应该明白std::move并没有移动任何东西。它只是将它的参数转换为右值引用,然后如果左侧有一个移动构造函数/赋值运算符,左侧会处理它(以及如何实际移动对象)。对于普通旧类型(PODs),std::move并没有真正做任何事情,所以旧的值仍然保持不变。你没有"物理"地移动任何东西。

是的,我明白了。但是在main()示例中,Bar()函数接受哪种类型的对象?它是引用吗(这解释了为什么我可以从main()更改foo对象)? - RippeR
1
@RippeR Bar(Foo&&) 接受一个对 Foo 的右值引用。std::move(foo) 将左值引用转换为对 Foo 的右值引用。在 Bar 中,您没有移动/更改任何内容,因此对象 foo 不会受到任何影响。尝试将 x.value = -1; 添加为 Bar(Foo&& x) 的最后一行,看看发生了什么。 - vsoftco

2

比较以下两个函数:

void Bar(Foo&& x) {
    std::cout << "# " << x.value << std::endl;
}

对比。

void Bar(Foo&& x) {
    std::cout << "# " << x.value << std::endl;
    Foo y=std::move(x);
}

两者都使用了右值引用,但只有第二个调用了移动构造函数。因此,第一个的输出结果是

# 5
5

第二个输出结果与第一个不同,因为 foo 的值已经被改变。

# 5
1

演示


编辑:这也是我曾经有过的一个问题。我的错误在于假设创建rvalue引用直接调用移动构造函数的调用。但是,正如此前所提到的,std::move在运行时不执行任何操作,它只是将类型更改为rvalue-reference。移动构造函数仅在您将rvalue引用“移动”到另一个对象中时才会被调用,如上所示。


1
谢谢,现在我明白Type&&只是另一个引用。我以为它是用移动构造函数构造新元素的。无论如何,感谢您的回答! :) - RippeR

1

Bar函数不应该使用引用 &&(这种语法只在移动构造函数的声明/定义中有效):

void Bar(Foo x) { ... }  // not "Foo&& x"

在函数Bar中调用临时参数对象的移动构造函数:

Bar(  std::move( x )  );

调用复制构造函数:
Bar( x );

我尊重地给这个回答点了踩,因为它说的是一个人“不应该”做什么,但原问题是在问为什么某件事情会以这种方式工作,而不是它是否是一个好的实践。这类问题很有价值,因为理解一个行为发生的原因可以加深我们对语言编程的理解,并提高我们理解代码在编译和运行时会实际执行什么的能力。 - Teeeeeeeeeeeeeeeeeeeeeeeeeeeej

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接