What constitutes a valid state for a "moved from" object in C++11?
Asked Answered
H

2

26

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?

Holzman answered 23/8, 2012 at 15:23 Comment(6)
The idea of move semantics is to take advantage of the work done to create non accessible items since they do not have a name. Why would you want to access a moved from item.Goodwin
@rerun: An intermediate container for storing stuff, of which the content is moved to another container (std::move(first, last, out)). You can then reuse the memory block inside the container (aka the "moved-from" elements).Gayton
@Gayton That is very specific example utilizing move for a performance reason and is very type specific in general a moved from object should be considered garbage.Goodwin
@Goodwin No, it definitely shouldn't. Ever wondered how the new std::swap works?Phyliciaphylis
@ChristianRau would you say then that a moved from object is garbage for access but a valid target of a move to operation.Goodwin
@Goodwin No, it is garbage for any access that assumes a certain state. For any others, like assignment (no matter if move or copy), destruction, or any non-prerequisite access or modification (e.g. clear, empty, begin, c_str) it is absolutely no garbage. That is the whole point of the good answers and the phrase "undefined but valid state".Phyliciaphylis
E
28

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).

Exist answered 23/8, 2012 at 15:46 Comment(1)
+1 For example, you can't pop_back a moved-from vector. You can't dereference a moved-from unique_ptr.Migrant
P
7

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.

Provincial answered 23/8, 2012 at 15:38 Comment(7)
+1 But out of curiousity, what "condition on the current state of the object" does push_back have that clear doesn't? From the user's perspective it seems somewhat arbitrary.Minnie
@Andrew: I don't think there's any implementation where calling push_back on a moved-from container will cause problems, but the standard specified that only certain operations after being moved from will not invoke UB.Gayton
push_back() doesn't have any preconditions so it should be fine, not UB.Exist
@NicolBolas: You probably meant pop_back, which does have a precondition.Migrant
@bames53: What about size() != max_size()?Leticialetisha
@CharlesBailey That's not actually specified as a precondition to push_back(). If push_back() can't allocate additional space for the element then it throws an exception and does not insert the element.Exist
@bames53: The call to push_back wouldn't be UB, but the resulting vector is unspecified, so it's not useful.Electrophoresis

© 2022 - 2024 — McMap. All rights reserved.