移动后右值是否有效?

Should the rvalue be valid after a move?

假设我们有以下 class:

class Foo
{
public:
    Foo() { _bar=new Bar };
    Foo(const Foo &right) { _bar=new Bar(right.bar); };
    Foo(Foo &&right) { _bar=right._bar;  right.bar=new Bar(); };

    ~Foo() { delete _bar; }

    Foo &operator=(const Foo &right) { _bar->opertor=(right.bar); return *this;}
    Foo &operator=(Foo &&right) { std::swap(_bar, right._bar); return *this;}

    void func() { _bar->test=1 };

private:
    Bar *_bar;
};

将其更改为以下内容并期望最终用户知道在执行移动后右值不再有效(因为调用赋值运算符以外的任何东西都可能崩溃)是否合法?

class Foo
{
public:
    Foo() { _bar=new Bar };
    Foo(const Foo &right) { _bar=new Bar(right.bar); };
    Foo(Foo &&right) { _bar=right._bar;  right.bar=nullptr; };

    ~Foo() { if(_bar != nullptr) delete _bar; }

    Foo &operator=(const Foo &right) 
    { 
         if(_bar == nullptr) 
             _bar=new Bar();
         _bar->opertor=(right.bar); 
         return *this;
    }

    Foo &operator=(Foo &&right)
    {
        if(_bar != nullptr)
            delete _bar;  
        _bar=right._bar; 
        right._bar=nullptr; 
        return *this;
    }

    void func() { _bar->test=1 };

private:
    Bar *_bar;
};

我担心的是 func(以及 class 中的所有其他函数)假设 _bar 存在。

is it legitimate to alter it to the following and expect the end user to know that after the move is performed that the rvalue is no longer valid?

你当然应该这样做。不应将右值引用保持在可用状态。移动构造函数和移动赋值运算符背后的想法是,您正在移动 对象中的有用数据。不过,我不会使用 不再有效 来描述它。从 C++ 的角度来看,它仍然是一个有效的对象,就像 nullptr 是一个有效的指针一样。

原则上它可能会变得无效,但您可能想考虑将其保留在可分配状态(您的原始实现,因此编辑,没有这样做)。这将遵循标准库的策略,即:

Unless otherwise specified, all standard library objects that have been moved from are placed in a valid but unspecified state. That is, only the functions without preconditions, such as the assignment operator, can be safely used on the object after it was moved from

我建议重新实现赋值运算符,以便用新构造的对象交换 "this" 对象。这通常是避免在执行分配时引入不正确行为的好方法。

第二个版本更加地道。一个对象从它移走后,就假定它不再被使用。

在指针上调用 delete 之前检查 nullptr 是不必要的,因为它在标准中定义为不执行任何操作。

如果用户希望使用移出的对象,则他的工作是确保它处于有效状态(即从有效对象分配)。

移出的对象应该处于有效但未指定的状态。请注意,这是建议而非标准的绝对要求。

如果之后对其执行正常操作(特别是复制赋值运算符),您的第二个代码将会中断。

如果 _bar == nullptr 是一个有效状态,那么你的复制赋值运算符有问题;如果它不是有效状态,那么我会说你的移动构造函数有问题。

注意。在第二个代码中,析构函数中的 if 检查是多余的,因为删除空指针是合法的。

您应该为您的 Foo 记录每个成员函数的先决条件,例如:

void Foo::func();

Requires: *this is not in a moved-from state.

那么您可以为任何需要使用 Foo::func() 的客户提供帮助,除非...

如果您将 Foo 与需要 func() 的其他库一起使用,并且没有按照 "except when in a moved-from state" 的方式记录某些内容,那么您就不走运了。

例如:假设您的 Foo 有一个像这样的 operator<

bool operator<(const Foo& x, const Foo& y);

Requries: Neither x nor y may be in a moved-from state.

现在,如果您执行以下操作:

std::vector<Foo> v;
// ... fill v
std::sort(v.begin(), v.end());  // oops!

上面最后一行要求 Foo LessThanComparable 无论 Foo 是否处于移出状态 。而std::lib中的泛型代码全部也是如此。 std 代码的 requires 子句适用于用户提供的代码,并且不会对移出的对象进行例外处理。

但是,如果您的 operator< 在任何一个参数处于移出状态时都有效,那么对 std::sort 的调用就可以了。即使 func() 仍然要求 Foo 处于移出状态,也是如此。这是因为 std::sort 根本不需要 func() 来工作。

总而言之,这完全取决于您的 Foo 与哪些其他代码交互。也许没关系,也许不是。记录 Foo 的作用,并了解您使用 Foo 的代码的要求。