What constitutes a valid state for a “moved from”

2019-01-03 03:14发布

I've been trying to wrap my head around how move semantics in C++11 are supposed to work, and I'm having a good deal of trouble understanding what conditions a moved-from object needs to satisfy. Looking at the answer here doesn't really resolve my question, because can't see how to apply it to pimpl objects in a sensible way, despite arguments that move semantics are perfect for pimpls.

The easiest illustration of my problem involves the pimpl idiom, like so:

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;
    }
};

Now, what can I do once I've moved from a Foo? I can destroy the moved-from object safely, and I can assign to it, both of which are absolutely crucial. However, if I try to do_stuff with my Foo, it will explode. Before I added move semantics for my definition of Foo, every Foo satisfied the invariant that it could do_stuff, and that's no longer the case. There don't seem to be many great alternatives, either, since (for example) putting the moved-from Foo would involve a new dynamic allocation, which partially defeats the purpose of move semantics. I could check whether impl_ in do_stuff and initialize it to a default FooImpl if it is, but that adds a (usually spurious) check, and if I have a lot of methods it would mean remembering to do the check in every one.

Should I just give up on the idea that being able to do_stuff is a reasonable invariant?

2条回答
Fickle 薄情
2楼-- · 2019-01-03 03:48

You define and document for your types what a 'valid' state is and what operation can be performed on moved-from objects of your types.

Moving an object of a standard library type puts the object into an unspecified state, which can be queried as normal to determine valid operations.

17.6.5.15 Moved-from state of library types                                         [lib.types.movedfrom]

Objects of types defined in the C++ standard library may be moved from (12.8). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.

The object being in a 'valid' state means that all the requirements the standard specifies for the type still hold true. That means you can use any operation on a moved-from, standard library type for which the preconditions hold true.

Normally the state of an object is known so you don't have to check if it meets the preconditions for each operation you want to perform. The only difference with moved-from objects is that you don't know the state, so you do have to check. For example, you should not pop_back() on a moved-from string until you have queried the state of the string to determine that the preconditions of pop_back() are met.

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

The state is probably unspecified because it would be onerous to create a single useful set of requirements for all different implementations of the standard library.


Since you are responsible not only for the specification but also the implementation of your types, you can simply specify the state and obviate the need for querying. For example it would be perfectly reasonable to specify that moving from your pimpl type object causes do_stuff to become an invalid operation with undefined behavior (via dereferencing a null pointer). The language is designed such that moving only occurs either when it's not possible to do anything to the moved-from object, or when the user has very obviously and very explicitly indicated a move operation, so a user should never be surprised by a moved-from object.


Also note that the 'concepts' defined by the standard library do not make any allowances for moved-from objects. That means that in order to meet the requirements for any of the concepts defined by the standard library, moved-from objects of your types must still fulfill the concept requirements. This means that if objects of your type don't remain in a valid state (as defined by the relevant concept) then you cannot use it with the standard library (or the result is undefined behavior).

查看更多
爷、活的狠高调
3楼-- · 2019-01-03 03:50

However, if I try to do_stuff with my Foo, it will explode.

Yes. So will this:

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

The rule the standard uses is to leave the object in a valid (meaning that the object works) but unspecified state. This means that the only functions you can call are those that have no conditions on the current state of the object. For vector, you can use its copy/move assignment operators, as well as clear and empty, and several other operations. So you can do this:

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.

For your case, copy/move assignment (from either side) should still work, as should the destructor. But all other functions of yours have a precondition based on the state of not having been moved from.

So I don't see your problem.

If you wanted to ensure that no instance of a Pimpl'd class could be empty, then you would implement proper copy semantics and forbid moving. Movement requires the possibility of an object being in an empty state.

查看更多
登录 后发表回答