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

Pil*_*lsy 24 c++ pimpl-idiom invariants move-semantics c++11

我一直在试图解决C++ 11中的移动语义应该如何工作,而且我很难理解移动对象需要满足的条件.看看这里答案并没有真正解决我的问题,因为无法看到如何以合理的方式将它应用于pimpl对象,尽管移动语义的参数非常适合pimpls.

我的问题最简单的说明涉及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;
    }
};
Run Code Online (Sandbox Code Playgroud)

现在,一旦我离开了,我该怎么办Foo?我可以安全地销毁移动的物体,我可以分配给它,这两者都绝对是至关重要的.但是,如果我尝试do_stuff使用我的Foo,它会爆炸.在我为我的定义添加移动语义之前Foo,每个人都Foo满足了它的不变性do_stuff,而现在已不再是这样了.目前似乎并没有被大量的替代品,或者说,因为(例如)将被移至距离Foo将包括一个新的动态分配,这部分违背了移动语义的目的.我可以检查是否impl_输入do_stuff并将其初始化为默认值,FooImpl如果是,但是这会增加(通常是假的)检查,如果我有很多方法,那就意味着要记住每一个检查.

我应该放弃能够do_stuff成为合理的不变量的想法吗?

bam*_*s53 24

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

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

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

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

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

通常,对象的状态是已知的,因此您无需检查它是否满足您要执行的每个操作的前提条件.移动对象的唯一区别是你不知道状态,所以你必须检查.例如,在查询字符串的状态以确定满足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
Run Code Online (Sandbox Code Playgroud)

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


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


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

  • +1例如,您无法pop_back移动的向量.您不能取消引用来自unique_ptr的移动. (3认同)

Nic*_*las 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
Run Code Online (Sandbox Code Playgroud)

标准使用的规则是将对象保留为有效(意味着对象有效)但未指定状态.这意味着您可以调用的唯一函数是那些对对象的当前状态没有条件的函数.对于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.
Run Code Online (Sandbox Code Playgroud)

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

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

如果你想确保没有Pimpl'd类的实例可以为空,那么你将实现适当的复制语义并禁止移动.运动需要物体处于空状态的可能性.

  • @NicolBolas:你可能意味着`pop_back`,**有前提条件. (5认同)
  • @ bames53:对`push_back`的调用不是UB,但结果向量是未指定的,所以它没用. (5认同)
  • push_back()没有任何先决条件,所以它应该没问题,而不是UB. (3认同)