什么构成C ++ 11中“移动”对象的有效状态?

时间:2012-08-23 15:23:50

标签: c++ c++11 move-semantics pimpl-idiom invariants

我一直在试图解决C ++ 11中的移动语义应该如何工作,而且我很难理解移动对象需要满足的条件。查看answer here并没有真正解决我的问题,因为尽管有move semantics are perfect for pimpls的论据,但无法看到如何以合理的方式将它应用于pimpl对象。

我问题的最简单说明涉及pimpl习语,如下:

class Foo {
    std::unique_ptr<FooImpl> impl_;
public:
    // Inlining FooImpl's constructors for brevity's sake; otherwise it 
    // defeats the point.
    Foo() : impl_(new FooImpl()) {}

    Foo(const Foo & rhs) : impl_(new FooImpl(*rhs.impl_)) {}

    Foo(Foo && rhs) : impl_(std::move(rhs.impl_)) {}

    Foo & operator=(Foo rhs) 
    {
        std::swap(impl_, rhs.impl_);

        return *this;
    }

    void do_stuff () 
    {
        impl_->do_stuff;
    }
};

现在,一旦我离开Foo,我该怎么办?我可以安全地销毁移动的物体,我可以分配给它,这两者都绝对是至关重要的。但是,如果我尝试使用do_stuff Foo,它会爆炸。在我为Foo的定义添加移动语义之前,每个Foo都满足了它do_stuff的不变量,并且不再是这种情况。似乎没有很多很好的选择,因为(例如)将移动的Foo放入将涉及新的动态分配,这部分地违背了移动语义的目的。我可以检查impl_中是否do_stuff并将其初始化为默认FooImpl(如果是),但是会增加(通常是假的)检查,如果我有很多方法,它会意思是记住每一个都要检查。

我是否应该放弃能够do_stuff是合理的不变量的想法?

2 个答案:

答案 0 :(得分:23)

您可以为您的类型定义和记录“有效”状态以及可以对类型的移动对象执行的操作。

移动标准库类型的对象会将对象置于未指定状态,可以正常查询该状态以确定有效操作。

  

17.6.5.15从库类型的状态移出   [lib.types.movedfrom]

     

可以移动C ++标准库中定义的类型的对象   (12.8)。可以明确指定或隐式指定移动操作   产生。除非另有规定,否则此类移动物体应   被置于有效但未指明的状态。

处于“有效”状态的对象意味着标准为该类型指定的所有要求仍然成立。这意味着您可以对前置条件成立的移动标准库类型使用任何操作。

通常,对象的状态是已知的,因此您无需检查它是否满足您要执行的每个操作的前提条件。移动对象的唯一区别是你不知道状态,所以你必须检查。例如,在查询字符串的状态以确定是否满足pop_back()的前提条件之前,不应对移动的字符串执行pop_back()。

std::string s = "foo";
std::string t(std::move(s));
if (!s.empty()) // empty has no preconditions, so it's safe to call on moved-from objects
    s.pop_back(); // after verifying that the preconditions are met, pop_back is safe to call on moved-from objects

状态可能未指定,因为为标准库的所有不同实现创建一组有用的需求将是繁重的。


由于您不仅要负责规范,还要负责类型的实现,因此您只需指定状态并避免查询的需要。例如,指定从pimpl类型对象移动会导致do_stuff成为具有未定义行为的无效操作(通过解除引用空指针)是完全合理的。该语言的设计使得移动仅在无法对移动的对象执行任何操作时发生,或者当用户非常明显且非常明确地指示移动操作时,移动才会发生,因此用户不应对移动的操作感到惊讶来自对象。


另请注意,标准库定义的“概念”不会对移动对象进行任何限制。这意味着,为了满足标准库定义的任何概念的要求,类型的移动对象仍必须满足概念要求。这意味着如果您的类型的对象不保持有效状态(由相关概念定义),那么您不能将它与标准库一起使用(或结果是未定义的行为)。

答案 1 :(得分:7)

  

但是,如果我尝试用我的Foo做do_stuff,它会爆炸。

是。那么这样:

vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.size();  //Value returned is undefined. May be 0, may not

标准使用的规则是将对象保留在有效(意味着对象有效)但未指定状态。这意味着您可以调用的函数是那些对该对象的当前状态没有条件的函数。对于vector,您可以使用其复制/移动赋值运算符,以及clearempty以及其他一些操作。所以你可以这样做:

vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.clear();  //Cause the vector to become empty.
first.size(); //Now the value is guaranteed to be 0.

对于您的情况,复制/移动分配(从任何一方)仍然可以工作,析构函数也应如此。但是你的所有其他功能都有一个基于未被移动状态的前提条件。

所以我没有看到你的问题。

如果您想确保没有Pimpl'd类的实例可以为空,那么您将实现正确的复制语义并禁止移动。运动需要对象处于空状态的可能性。