26

我一直在努力思考 C++11 中的移动语义应该如何工作,并且在理解移动对象需要满足哪些条件时遇到了很多麻烦。在这里查看答案并不能真正解决我的问题,因为尽管移动语义的论点非常适合 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是一个合理的不变量的想法吗?

4

2 回答 2

28

您为您的类型定义和记录什么是“有效”状态以及可以对您的类型的移出对象执行哪些操作。

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

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 成为具有未定义行为的无效操作(通过取消引用空指针)是完全合理的。该语言的设计使得移动仅发生在无法对移动对象执行任何操作时,或者当用户非常明显且非常明确地指示移动操作时,因此用户永远不应该对移动的对象感到惊讶-从对象。


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

于 2012-08-23T15:46:17.487 回答
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,您可以使用它的复制/移动赋值运算符,以及clearandempty和其他几个操作。所以你可以这样做:

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 类的任何实例都不能为空,那么您将实现正确的复制语义并禁止移动。运动需要物体处于空状态的可能性。

于 2012-08-23T15:38:18.333 回答