C++11中std::move操作的行为

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

struct A {
    std::string s;
    A() : s("test") {}
    A(const A& o) : s(o.s) { std::cout << "move failed!\n"; }
    A(A&& o) : s(std::move(o.s)) {}
    A& operator=(const A&) { std::cout << "copy assigned\n"; return *this; }
    A& operator=(A&& other) {
        s = std::move(other.s);
        std::cout << "move assigned\n";`enter code here`
        return *this;
    }
};

A f(A a) { return a; }

struct B : A {
    std::string s2;
    int n;
    // implicit move assignment operator B& B::operator=(B&&)
    // calls A's move assignment operator
    // calls s2's move assignment operator
    // and makes a bitwise copy of n
};

struct C : B {
    ~C() {}; // destructor prevents implicit move assignment
};

struct D : B {
    D() {}
    ~D() {}; // destructor would prevent implicit move assignment
    //D& operator=(D&&) = default; // force a move assignment anyway 
};

int main()
{
    A a1, a2;
    std::cout << "Trying to move-assign A from rvalue temporary\n";
    a1 = f(A()); // move-assignment from rvalue temporary
    std::cout << "Trying to move-assign A from xvalue\n";
    a2 = std::move(a1); // move-assignment from xvalue

    std::cout << "Trying to move-assign B\n";
    B b1, b2;
    std::cout << "Before move, b1.s = \"" << b1.s << "\"\n";
    b2 = std::move(b1); // calls implicit move assignment
    std::cout << "After move, b1.s = \"" << b1.s << "\"\n";

    std::cout << "Trying to move-assign C\n";
    C c1, c2;
    c2 = std::move(c1); // calls the copy assignment operator

    std::cout << "Trying to move-assign D\n";
    D d1, d2;
//  d2 = std::move(d1);
}

执行a2 = std::move(a1)语句时,其行为与执行b2 = std::move(b1)语句的行为不同。在下面的语句中,移动操作后b1.s并未变为空,而a1.s变为空。
有人可以解释一下这到底发生了什么吗?

你使用的编译器是什么? - Praetorian
@Praetorian 或者说,你为什么认为你的编译器符合C++11标准? - Yakk - Adam Nevraumont
2
@Yakk 的意思是这可能是 gcc 引用计数的 std::string 实现的副作用,它不符合 C++11 标准。无论如何,对于已移动的对象,除了处于有效但未指定状态之外,没有任何保证,因此库可能将移动赋值实现为调用 swap,这会使源包含目标包含的任何内容,或者它可能将其实现为调用 assign(或类似的东西),这会使源字符串为空。 - Praetorian
3
@praet或msvc2013,它们不会自动生成移动构造函数。有很多理由不信任编译器。此外,我们还有SSO,可以使短距离移动变成复制操作。 - Yakk - Adam Nevraumont
2个回答

2

关于C++11和右值引用的一个常见误解是,std::move会对对象进行某些操作(或类似操作)。

实际上并不是这样的。 std::move只是将其参数强制转换为右值引用类型,并返回该类型。对对象所做的任何操作都发生在移动构造函数、移动赋值运算符(等等)中,这是因为调用了以右值引用为参数的版本(而不是以值或左值引用为参数的版本)。

至于你提出的具体问题,至少根据你代码中的注释来看,你似乎存在一些误解。关于a2=std::move(a1);的注释说你正在从xvalue进行“移动赋值”。这个说法最好是误导性的。xvalue是一个即将立即过期的值。它几乎只用于函数的返回值:

Foo &&bar() { 
    Foo f;
    // ...
    return f;
}

在本例中,bar() 是一个 xvalue ,因为 bar 返回一个右值引用指向一个在函数执行结束时过期(超出范围)的对象。

至于你所问的具体问题,我怀疑它主要取决于你的标准库是否实现了 std::string 的移动构造函数。例如,当使用 g++(4.9.1) 时,我得到了和你一样的结果--b1.s 在被用作移动源之前和之后都包含 test。另一方面,如果我使用 MS VC++ 14 CTP,我在移动之前得到了 b1.s="test",移动之后得到了 b1.s=""。虽然我没有测试过,但我希望使用 Clang 的结果是相同的。简而言之,看起来 gcc 的标准库并没有真正实现 std::stringmove 赋值/构造函数(至少在 v4.9 中,我还没有看过 5.0)。

3
bar()函数中,f不是一个xvalue,而是一个lvalue。在这种情况下,有一条特殊规则,它指定了在重载解析中,“好像对象被rvalue指定”(请注意,这是逆条件句)。对于A f(A a) { return a; }f(A())的结果是prvalue。 - T.C.
糟糕——谢谢——本意是返回一个右值引用,但不知何故在返回类型上漏掉了关键的 &&... - Jerry Coffin
表达式 bar() 是一个 xvalue,但 bar() 中的 f 不是。 - T.C.

1
通常移动赋值被实现为在std::string上进行交换,那么既然它始终使用"test"进行初始化,为什么字符串会变为空呢?
你在哪里看到a1.s变为空了,因为没有打印出来吗?
我没有看到任何奇怪的行为这里。两者都以相同的方式处理。

@ Jack,我在调试代码时观察到了这一点。move操作后,a1.s的值发生了变化,但b1.s的值没有变化,它们都是字符串。虽然a1和b1都是左值,但我使用std::move()将它们转换为右值……所以为什么会出现这种情况呢?而且,在第一种情况下,它没有调用复制构造函数,而在第二种情况下,它调用了复制构造函数。我能知道std::move()在派生类中的工作原理吗? - Sahithi Mamidi

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