Moving an Object Does Not Destroy The Original
Last week and the week before that, I started discussing how a C++ compiler uses the type system to figure out when to move an object and when to copy it. Now I'd like to take a detour to discuss what moving an object actually does.
Recall first that objects are generally moved instead of copied because the originals are about to go away. When I learned this fact, I thought at first that moving an object should destroy the original. For example:
Thing make_Thing(); // … Thing t = make_Thing();
In the last line of this example, the result returned by make_Thing
is used to initialize the variable t
, and is not used again after that. Therefore, that result can be moved instead of copied. Surely it makes sense to destroy the result as part of the move process!
Because of this reasoning, I was surprised to learn that that is not what happens. Instead, whenever a program moves an object, it must leave that object with a valid value so that it can be destroyed later. In general, this rule causes additional overhead, because it means that moving an object must typically install a new value in the original object, and all that happens to that new value is that it is destroyed later.
What I learned surprised me because in this particular example, the actual behavior seems to add overhead. As part of moving the temporary Thing
returned by make_Thing
into t
, the generated code gives a new value to the temporary Thing
, and then destroys that temporary Thing
. Why bother? Why not just destroy the temporary Thing
immediately?
In principle, I think it would have been possible to make C++ work that way. However, the way C++ actually does things makes life easier for C++ developers in an important way: It allows objects to be moved explicitly. For example:
Thing make_Thing(); void process_Thing(Thing);
Assume that the process_Thing
function really does accept a Thing
by value (i.e., type Thing
) rather than by reference to const
(i.e., type const Thing&
). You might want to write code along these lines:
Thing t = make_Thing(); // … process_Thing(t);
Suppose you know that t
is not going to be used again after passing it to process_Thing
. If you were able to write
process_Thing(make_Thing());
then the compiler would move the result of make_Thing
to Process_Thing
. Unfortunately, you want to work on t
between when you create it and when you pass it to process_Thing
, so you can't write the code this way.
In cases such as this, C++ allows you to write
process_Thing(std::move(t));
When you do this, you are promising to the compiler that you are not going to use t
again, so the compiler can move t
rather than copying it.
You may ask: Why can't the compiler recognize std::move
as a special case that causes t
to be destroyed? It could notice this statement and simply not destroy t
afterward, knowing that std::move
had already done so.
The trouble is that the call to process_Thing
is a statement, which means that it can be made conditional:
Thing t = make_Thing(); // … if (…) process_Thing(std::move(t));
Depending on the condition in the if
statement, it is now somewhere between difficult and impossible for the compiler to figure out whether std::move(t)
has been called. If calling std::move(t)
were to destroy t
, the compiler would not know whether it was safe to destroy t
at the end of the block containing this statement. So by requiring an object to have a valid value after its contents have been moved elsewhere, C++ makes it unnecessary for the compiler to track at runtime whether the object has been destroyed.
In short, after you move an object's contents to a new object:
- The new object contains the same value as the original object did before the move; and
- The original object contains a valid value that can (and should) be destroyed at the end of the original object's lifetime.
Notice that these rules say nothing about what the original object's value should be after the move. In practice, there are three common strategies, each of which is particularly easy to implement in some circumstances:
- The original object takes on an empty value of some kind.
- The original object retains its former value, effectively implementing move as copy.
- The original object gets the former value of the new object, effectively implementing move as swap.
This third possibility may be surprising, but consider that we know that the original object will be destroyed eventually. If moving an object t
to another object s
must overwrite the previous value of s
, it may turn out to be more convenient to implement this operation by swapping the contents of s
with those of t
, knowing that t
(which, after the swap, holds the original value of s
) will be destroyed eventually.
In short, the C++ move operation may be surprising in that it does not destroy the original. However, along with that behavior comes several nice properties:
- The compiler does not have to figure out whether an object might have been destroyed before the end of its natural life.
- It is always possible to use a copy operation in place of a move operation; i.e., to implement
move
ascopy
. - It is always possible to use a swap operation in place of a move operation' i.e., to implement
move
asswap
.
Next week we'll shift our focus back to the type system and take a look at how the std::move
function might work.