C++ and Zombies: a moving question
published at 31.10.2014 11:53 by Jens Weller
This has been on my things to think about since C++Now. At C++Now, I realized, that we've might got zombies in the C++ standard. And that there are two fractions, one of them stating, that it is ok to have well defined zombies, while some people think that you'd better kill them.
The first real aha moment came to me at Eric Nieblers Meeting C++ Keynote, where he did talk about C++11 and library design. Eric claimed, that when moving, the moved from object should be left behind in a well defined state. At this point I already understood, that std::move is not a function, but actually a cast to an r-value, but the thought, that if I'd move an object from A to B, that after the move A still exists, and is not destroyed, was an interesting one to me.
So, Eric Niebler shed some light on, that when you implement move constructors and assignment operators, you actually not just have to care about the move, but also about what is left behind. If you don't, you might create a zombie. A object, whose value (aka life) has moved somewhere else, but pointers might still point somewhere. eww.
So, his guideline is:
Moved-from objects must be in a valid but unspecified state
Now comes C++Now, and again Eric is giving his keynote, this time as a normal talk: C++11 Library Design. I joined to watch it a second time (as an organizer its hard to follow any talks), and how people at C++Now would react to his ideas for writing libraries in the age of C++11 and beyond. There was actually some (expected) discussions, after all his talk was received very well. Again, regarding move, he thinks that the left behind object must be in a well defined and valid state.
What makes the whole topic worth thinking about, is that Sean Parent argued in his talk Goals for Better Code - Implement Complete Types for destructive move. He claims that it is much better for performance to destroy the object, and its also for many people the semantically valid option: if you move a variable from A to B, its in B, not anymore in A. A should not exist as an object after the move. The following discussion was very interesting, and as I already mentioned gave me something to think about.
Who is right? Whats the correct thing to do with a moved-from object?
I don't like zombies
I exchanged at CppCon thoughts on this with very few people, but none could give an answer. Later I realized, I didn't ask the one man, who could shine some light on this issue: Howard Hinnant. I'll get to his answer, but first, I think we can agree on, that nobody wants to have zombies in C++, as Howard Hinnant pointed out the current requirements for a move-from object:
According to C++11/14, a moved from object must continue to meet whatever requirements the algorithm that is executing originally demanded of the non-moved-from object.
Currently, Eric Niebler is (a little more) correct, as the standard has actually requirements on a moved-from object, so if move is not destructive, you must also care about the object you move from. Howard used std::sort as an example, why this is important: if you sort a vector, you clearly want to move objects, but none of them need to be destroyed.
But, this does not mean, that Sean Parent is wrong! This is what makes this so worth thinking about, Sean Parent is way to smart, to argue for destructive move, if there wasn't a point worth doing so. Semantically he is correct, that also a destructive move should be possible. He has also published a non-proposal for destructive move on how this could be achieved in the standard. And, there is now a proposal for adding destructive move as an option into the standard.
So, as it turns out, neither Eric nor Sean are wrong. Eric is correct, that he points out the requirements imposed on types by the C++ standard. Sean is right, that in some cases, it is much better to actually destroy what is left behind, as no algorithm is placing a requirement on this object.
Howard Hinnant used an example I'd like to share with you:
For example, if you are calling:
sort requires that X be Swappable, MoveConstructible, MoveAssignable, and LessThanComparable. sort requires these things of X whether or not X is in a moved-from state. This is actually a little bit stricter than necessary because no sane sort algorithm would compare a moved-from X. But nevertheless, the committee has not relaxed the requirements along these lines.
Even if the committee does in the future decide that std::sort does not require LessThanComparable of moved-from X’s, moved-from X’s will still need to be Swappable, and MoveAssignable-to lest the sort algorithm will not be able to get its job done.
As the author of X, you can leave X in whatever state you wish after it is moved from. But you should document what operations on X require the X to not be in a moved-from state. And if you use X in a std::algorithm, then you should ensure that it meets the requirements of that algorithm whether or not it is moved-from.
So, its actually on the author what to do. The only thing you really shouldn't do, is creating zombies. The standard currently does not support destructive move operations, but they are a valuable addition to the current options.
Howard continues to point out a few more interesting points, which I'd like to share with you:
Flipping this around, one can say that if you have a moved-from object, you can do any operation with that object which has no preconditions. But the author of said object is free to put a precondition on his object that says: You can not do operation Y on object X if X is in a moved-from state.
Most std-defined types have an unspecified state when moved-from. For example if you move a vector, the moved-from vector will be in a valid but unknown state. You can do anything with that vector that does not have any preconditions. You can destruct it.
You can assign it a new value. You can ask its size() or capacity(). But you can not pop_back() it. Why? Because pop_back() has a precondition that the vector must not be empty. And it might be. If you first check the moved-from vector, and discover it is not empty, then you can pop_back() it. In summary, the standard does not treat the moved-from state as special. It simply treats it as an unspecified (but still constructed) state.
Destructive move semantics can peacefully coexist with the current standard move semantics. But it will (presumably) leave the moved-from object in a destructed state.
The concept of an unspecified but valid state is not actually new to C++11. It exists in C++98/03 as well. For example if you copy assign a vector and an exception is thrown from that operation, the lhs vector will have a valid but unspecified state if the exception is caught before the lhs destructs. Exact same idea for moved-from objects in C++11/14.
Also I want to point out, while move-semantics are a C++11 feature, but have been available long before 2011. For a long time Adobes ASL (2005) and later also boost::move (2008) made move operations available before C++11, and also Sean Parent showed in his talk, how he implemented his destructive move, which originally is an idea of Alex Stepanov. According to Sean Parent, the original ideas for moving objects came from Dave Abrahams and Howard Hinnant, the proposal N1377 from 2002 shows the original thoughts on move-semantics. Still, it took 9 years to become part of the C++11 standard.
So for most types, you should trust the default implementation for the move constructors and assignment operators.