What is left in a variable after using std::move on it?
Asked Answered
T

4

22

After using std::move in a variable that might be a field in a class like:

class A {
  public:
    vector<string>&& stealVector() {
      return std::move(myVector);
    }
    void recreateMyVector() {
    }
  private:
    vector<string> myVector;        
};

How would I recreate the vector, like a clear one? What is left in myVector after the std::move?

Talesman answered 31/12, 2013 at 1:13 Comment(10)
Please don't return rvalue references...Satanism
Note that std::move doesn't actually move. It is just a cast to an xvalue (rvalue).Hollowell
How would you do this then? If the caller asks to steal my vector, I don't need its contents anymore, but I might need the object to store new things.Talesman
Yes, that is not an appropriate use of an rvalue reference. If you want it to move the vector out of the class, you should return by value.Sisterinlaw
Congratulations, now with a call to stealVector your class is in an unspecified state.Grogan
@Jefffrey: is it? I see a couple of references being tossed around but if nothing is doing anything with them I don't think anything changes. After all std::move(myVector) doesn't really change anything...Alarm
@DietmarKühl, well, if there's an std::vector awaiting the return call from stealVector (which is the whole purpose of the function), then yes, it is.Grogan
possible duplicate of Reusing a moved container?Unkennel
@KerrekSB: why do you said to not return rvlaue references, is it illegal c++ or just bad practice ?Tso
@Guillaume07 - it's bad practice. You almost always want to return/pass by value to use move semantics. The answer to this question explains why: Is returning by r-value reference more efficient. You might also be able to leverage a "return by r-value reference" better if you were doing some template meta-programming with reference collapsing, though that seems like an obscure case for most users.Cusick
S
21

The common mantra is that a variable that has been "moved-from" is in a valid, but unspecified state. That means that it is possible to destroy and to assign to the variable, but nothing else.

(Stepanov calls this "partially formed", I believe, which is a nice term.)


To be clear, this isn't a strict rule; rather, it is a guideline on how to think about moving: After you move from something, you shouldn't want to use the original object any more. Any attempt to do something non-trivial with the original object (other than assigning to it or destroying it) should be carefully thought about and justified.

However, in each particular case, there may be additional operations that make sense on a moved-from object, and it's possible that you may want to take advantage of those. For example:

  • The standard library containers describe preconditions for their operations; operations with no pre­conditions are fine. The only useful ones that come to mind are clear(), and perhaps swap() (but prefer assignment rather than swapping). There are other operations without preconditions, such as size(), but following the above reasoning, you shouldn't have any business inquiring after the size of an object which you just said you didn't want any more.

  • The unique_ptr<T, D> guarantees that after being moved-from, it is null, which you can exploit in a situation where ownership is taken conditionally:

    std::unique_ptr<T> resource(new T);
    std::vector<std::function<int(std::unique_ptr<T> &)> handlers = /* ... */;
    
    for (auto const & f : handlers)
    {
        int result = f(resource);
        if (!resource) { return result; }
    }
    

    A handler looks like this:

    int foo_handler(std::unique_ptr<T> & p)
    { 
        if (some_condition))
        {
            another_container.remember(std::move(p));
            return another_container.state();
        }
        return 0;
    }
    

    It would have been possible generically to have the handler return some other kind of state that indi­cates whether it took ownership from the unique pointer, but since the standard actually guaran­tees that moving-from a unique pointer leaves it as null, we can exploit that to transmit that information in the unique pointer itself.

Satanism answered 31/12, 2013 at 1:15 Comment(9)
"but nothing else" That's not quite correct: You can use all member functions that have no preconditions (like const observers, typically). See #7028023Hollowell
@DyP: Well, any specific class can of course make any number of additional guarantees. I was talking about the general way that move should be thought about when encountered in code.Satanism
As far as I understand it, the "valid, but unspecified state" does imply that: It's not an invalid state, so you can still use the object, you just don't know what state it is in (as in: which state of a set of valid states). Therefore, any (member) function that does not require the state to be one of a specific set can be used on a moved-from object.Hollowell
@DyP: Then again, you can't tell from a public interface whether a member function accesses a state, and this is rarely guaranteed contractually. So while your reasoning is sound, I'm unsure whether it's widely applicable. Again, this is how you should think about move, not a precise set of operational constraints.Satanism
In addition to destroy and assign to, you can do anything else with the std::vector that has no preconditions, such as myVector.clear(). myVector is "fully formed", not "partially formed". You just don't know what fully formed state it is in, unless you inspect it with queries requiring no precondition (e.g. myVector.size(), myVector.empty()). Or if you force it to a specified state with an operation requiring no precondition (e.g. myVector.clear(), myVector = otherVector, myVector.~vector<T>()).Fleshy
@KerrekSB "I'm unsure whether it's widely applicable." It's applicable to everything in the standard library and I believe that third party code which uses the standard library must also adhere to the 'valid but unspecified state' rule, at least with regards to the specific requirements placed on the third party type by the standard library component. In both these cases there are contractual guarantees: those specified by the standard.Unkennel
Also the relevant fact isn't whether an operation accesses the state, but whether it has preconditions as to the object's state. .clear() obviously accesses the state, but is necessarily valid on a moved-from vector. Third party libraries might not document how they access internal state, but it would be very bad for one to not even tell you when you can legally use the operations it provides. I can't see that such a library could be used reliably even without considering moves.Unkennel
@bames53: I agree that expressing it in terms of preconditions is the right way to think about it; I was almost going to add that, but then the only thing I could think of that would be advisable on a standard container is clear, and maybe swap. (But instead of swap you should assign.) The thing is, even if empty and size have no preconditions, you shouldn't want to use those after moving, if you see what I mean. If you move, you have no business to then have non-trivial interactions with the thing. Let me add some text, though.Satanism
@KerrekSB I agree that one generally doesn't need to do non-trivial things with the value of a moved from object (though I wouldn't be surprised if someone eventually discovers a reason), so recommending against it seems fine to me. However I still feel that the rule should be described accurately. IMO the sentence "That means that it is possible to destroy and to assign to the variable, but nothing else," should be replaced with a correct description, and then the text below can continue on with your recommendations.Unkennel
D
12

Move the member vector to a local vector, clear the member, return the local by value.

std::vector<string> stealVector() {
    auto ret = std::move(myVector);
    myVector.clear();
    return ret;
}
Dearing answered 31/12, 2013 at 1:25 Comment(4)
This shows by example how to clear the vector after moving from it, and shows a much better option than returning a reference, but doesn't answer the question about what's left in the vector after moving from it.Unkennel
+1 anyway, for showing a better alternative to returning a reference.Unkennel
is calling clear() allowed after the move? Couldn't that lead to undefined behavior? I'd replace it with myVector = std::vector<string>();Burke
@martinus: No, there is no undefined behavior. clear() has no preconditions.Dearing
S
5

Since I feel Stepanov has been misrepresented in the answers so far, let me add a quick overview of my own:

For std types (and only those), the standard specifies that a moved-from object is left in the famous "valid, but unspecified" state. In particular, none of the std types use Stepanov's Partially-Formed State, which some, me included, think of as a mistake.

For your own types, you should strive for both the default constructor as well as the source object of a move to establish the Partially-Formed State, which Stepanov defined in Elements of Programming (2009) as a state in which the only valid operations are destruction and assignment of a new value. In particular, the Partially-Formed State need not represent a valid value of the object, nor does it need to adhere to normal class invariants.

Contrary to popular belief, this is nothing new. The Partially-Formed State exists since the dawn of C/C++:

int i; // i is Partially-Formed: only going out of scope and
       // assignment are allowed, and compilers understand this!

What this practically means for the user is to never assume you can do more with a moved-from object than destroy it or assign a new value to it, unless, of course, the documentation states that you can do more, which is typically possible for containers, which can often naturally, and efficiently, establish the empty state.

For class authors, it means that you have two choices:

First, you avoid the Partially-Formed State as the STL does. But for a class with Remote State, e.g. a pimpl'ed class, this means that to represent a valid value, either you accept nullptr as a valid value for pImpl, prompting you to define, at the public API level, what a nullptr pImpl means, incl. checking for nullptr in all member functions.

Or you need to allocate a new pImpl for the moved-from (and default-constructed) object, which, of course, is nothing any performance-conscious C++ programmer would do. A performance-conscious C++ programmer, however, would also not like to litter his code with nullptr checks just to support the minor use-case of a non-trivial use of a moved-from object.

Which brings us to the second alternative: Embrace the Partially-Formed State. That means, you accept nullptr pImpl, but only for default-constructed and moved-from objects. A nullptr pImpl represents the Partially-Formed State, in which only destruction and assignment of another value are allowed. This means that only the dtor and the assignment operators need to be able to deal with a nullptr pImpl, while all other members can assume a valid pImpl. This has another benefit: both your default ctor as well as the move operators can be noexcept, which is important for use in std::vector (so moves and not copies are used upon reallocation).

Example Pen class:

class Pen {
    struct Private;
    Private *pImpl = nullptr;
public:
    Pen() noexcept = default;
    Pen(Pen &&other) noexcept : pImpl{std::exchange(other.pImpl, {})} {}
    Pen(const Pen &other) : pImpl{new Private{*other.pImpl}} {} // assumes valid `other`
    Pen &operator=(Pen &&other) noexcept {
        Pen(std::move(other)).swap(*this);
        return *this;
    }
    Pen &operator=(const Pen &other) {
        Pen(other).swap(*this);
        return *this;
    }
    void swap(Pen &other) noexcept {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
    int width() const { return pImpl->width; }
    // ...
};
Stallard answered 30/5, 2018 at 6:26 Comment(1)
Every day I am more convinced about embracing partially formed states. To the point that now I think containers should be allowed in a partially formed state if the contained value is allowed to be in a partially formed state. For example std::vector v(20) should contain objects in partially formed state (if T t is in a partially formed state). Else we always can do "std::vector v(20, {});". In practice std::vector v(20) should call std::uninitialized_default_construct, and std::vector v(20, {}) should call std::uninitialized_value_construct. What do you think?Towel
R
3

What is left in myVector after the std::move?

std::move doesn't move, it is just a cast. It can happen that myVector is intact after the call to stealVector(); see the output of the first a.show() in the example code below. (Yes, it is a silly but valid code.)

If the guts of myVector are really stolen (see b = a.stealVector(); in the example code), it will be in a valid but unspecified state. Nevertheless, it must be assignable and destructible; in case of std::vector, you can safely call clear() and swap() as well. You really should not make any other assumptions concerning the state of the vector.

How would I recreate the vector, like a clear one?

One option is to simply call clear() on it. Then you know its state for sure.


The example code:

#include <initializer_list>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class A {
  public:
    A(initializer_list<string> il) : myVector(il) { }
    void show() {
      if (myVector.empty())
        cout << "(empty)";
      for (const string& s : myVector)
        cout << s << "  ";
      cout << endl;
    }
    vector<string>&& stealVector() {
      return std::move(myVector);
    }
  private:
    vector<string> myVector;        
};

int main() {
  A a({"a", "b", "c"});
  a.stealVector();
  a.show();
  vector<string> b{"1", "2", "3"};
  b = a.stealVector();
  a.show();
}

This prints the followings on my machine:

a b c
(empty)

Roundup answered 31/12, 2013 at 13:8 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.