Move Safety – Know What Can Be Done in the Moved-From State
C++ programmers have this notion of exception safety. It is a very useful concept. With it one can easily describe the post-conditions of a function if it throws.
There is another situation where you need to easily describe some post-conditions: when talking about the state of an object after a move operation, i.e. after a move constructor or move assignment operator. I thus want to introduce vocabulary for those post-conditions of the right-hand argument similar to the exception safety of a function: The move safety, if you will.
The exception safety describes the post-conditions of a function if the function throws an exception. Similarly, the move safety describes the post-conditions of the object after a move operation. It thus gives information about what can be done safely with a moved-from object.
Why do you need move safety?
With move semantics you can specify more efficient code if you need to copy an object but don’t need the original object anymore. You can simply steal the other object’s resources, it will be thrown away after it anyways. The object that will be moved is an rvalue, thus it is temporary and will be destroyed shortly after. So why is it necessary to specify its state after the move operation?
Because not every time a move constructor or assignment operator will be called, the original object is a pure temporary.
Sometimes they are invoked with “artificial” temporaries created by a cast - which is what std::move()
does.
In those cases you might want to use the object again after the operation.
Then it is useful to know what exactly you can do with it.
For that I propose the concept of move safety. I’ve identified four levels, very similar to the exception safety levels and thus following the same naming scheme.
You might decide that there should be more levels - or less.
Those levels are in decreasing order of safety:
1. No-move guarantee: Copy only
A type provides no-move safety if its move constructor or assignment operator does not perform any move operations. Move for those types is equivalent to copy.
If a type provides this guarantee, it does not have any fancy user-defined move operations and only has members which provide this guarantee as well. Such types usually do not own any resources which need to be freed, so they do not have special copy operations or destructors.
Any trivial type has this guarantee as has any type where no move operations are implicitly generated.
2. Strong move safety: Well-defined and valid moved-from state
The moved-from state of a type that provides the strong move safety is well-defined. You can safely call all member functions whose pre-conditions are fulfilled by the defined state. In addition, those member functions will have deterministic effects/results.
With “deterministic” I mean effects or results that can be expected given the documentation behavior of the function.
An example of a type that provides the strong move safety is std::unique_ptr
.
Move construction is defined as a “transfer of ownership” which itself is defined in [unique.ptr]/4
Additionally,
u
can, upon request, transfer ownership to another unique pointeru2
. Upon completion of such a transfer, the following postconditions hold:
u2.p
is equal to the pre-transferu.p
,
u.p
is equal tonullptr
, andif the pre-transfer
u.d
maintained state, such state has been transferred tou2.d
.
So after a std::unique_ptr
is moved, it does not own anything.
operator bool()
will return false
, get()
will return nullptr
and you must not call operator*()
or operator->()
.
3. Basic move safety: Valid but unspecified moved-from state
The basic move safety doesn’t require a well-defined moved-from state. It only requires that the moved-from state is valid, but the exact state is not specified. You can safely call all member functions with a wide contract, i.e. no special pre-conditions on the object. But it is not guaranteed which results those functions will return; they’re effects/results are not deterministic as they were in the strong move safety.
An example of a type that provides the basic move safety is std::string
.
Let’s take a look at the following code for that:
auto a = "Hello World!"s;
auto b = std::move(a);
std::cout << a.c_str() << '\n';
What is the output of this program?
-
(empty line)
-
Hello World!
-
C++ is weird
-
(segmentation fault)
I didn’t come up with that quiz, I just can’t find the original source…
The answer is: std::string::c_str()
has no pre-condition and the object is left in a valid state, so it won’t be option 4.
You can safely call the function.
But it could be any of the other answers.
If the string Hello World!
was dynamically allocated by the std::string
,
move construction will likely only adjust the pointers, so the moved-from object is empty and it will output option 1.
But most implementations of std::string
use something called the small-string optimization (SSO).
Then they have a static buffer where they can store small strings without dynamic allocation.
In this case the move constructor cannot do a more efficient move than manually copying each character from one SSO buffer to the other SSO buffer.
And to be more efficient the implementation might not zero out the stolen buffer.
In this case the output will be option 2.
And for a really weird implementation the output might be option 3.
So the resulting state is valid, but you do not know it exactly.
The basic move guarantee is also what the standard library guarantees for all types unless otherwise specified.
4. No move safety: “Destructive” move
Update: There was some discussion about calling it destructive move, because it isn’t really a destructive move per se. Some also suggest further levels down, but I personally don’t see the point.
The least guarantee provides the no move safety: The moved from object isn’t valid anymore. You can only call its destructor or assign it a new value.
Note that this is more than the “no exception safety”, which does not guarantee anything about the post-conditions. But because move operations occur automatically from temporaries, you must not provide a move operation which doesn’t permit a destructor call – the compiler will call it on its own!
And because an assignment is conceptually the same as destroy and create again, I decided that it should be allowed as well.
After all you shouldn’t write types that do not have any guarantees in the moved-from state.
Which guarantee should I provide for my types?
For types that do not own any resources, they will automatically provide the no move guarantee.
For types that do own resources - where you actually need a move operation - provide the guarantee that is the fastest to implement while still sensible. Move operations can be seen as optimizations for copy. As such they should be as fast as possible. If you can easily implement the strong move safety, do it. If it is more work than the basic move safety, consider providing only the basic safety. The basic safety is obviously less useful than the strong safety because you do not know which state the object has, so only do it if you must.
Types that own a resource have two fundamental states: they own a resource or they don’t own a resource. Moving (or default construction) puts them in the state where they don’t own a resource. But for some types the state where they don’t own a resource isn’t feasible, it isn’t valid. For those types you should only implement destructive move: The state without a resource is invalid, so you must not do anything with it.
Conclusion
Move safety can be a useful definition. With it you can easily classify the moved-from state of an object. You should choose to support the safest level that can be implemented without overhead, or intentionally avoid any usable moved-form state by choosing destructive move.
The concept of move safety can easily provide answers to those Stackoverflow questions. Furthermore it helps in the documentation of your own types.
While writing this blog post I’ve realized a couple of things regarding default-construction. You can find a follow-up here: Move Semantics and Default Constructors – Rule of Six?.