More Thoughts About Moving Objects Safely
Moving objects instead of copying them is a tricky notion to explain — and perhaps trickier than I realized, given how many questions I received by email about last week's article.
The question seems simple enough: When is it safe to move an object instead of copying it? Where the trickiness comes in is in trying to define exactly what the question means. Clearly, if I am the author of a class, or I understand thoroughly how that class works, I get to decide what it means for it to be safe to move an object of my class instead of copying it. So that must not be the important question. Rather, the important question is this:
If a programmer writes code that looks like it is copying an object, when is it safe for the compiler quietly to change that copy into a move?
Here, "safe" means that the change will not affect the program's results; just its performance. I understand that this definition is vague at the moment, but I expect to make it less so in future articles. The real point is that we are talking about a decision made by the compiler, not by any programmer; and we are looking for circumstances that
- Allow the compiler to make the decision to change a copy into a move without worrying about whether that decision might break code; and
- Are easy enough to define that we can be confident that the compiler will actually make these decisions without needing to rely on sophisticated optimization and program-flow-tracing techniques.
With these constraints in mind, let's look again at last week's example:
Thing t; work_on(t);
Here, we are assuming that work_on
takes a Thing
parameter (not a reference to a Thing
, which would call for neither copying nor moving), and that we wrote neither the Thing
class nor the work_on
function and therefore do not know about their internals. We are asking whether the compiler can move t
to work_on
's parameter instead of copying it.
Let's start by isolating this code:
{ Thing t; work_on(t); }
By enclosing the code in curly braces, we make t
a local variable in a block. As a result, t
must be destroyed at the }
, so we can be confident from this code that there is no possibility of t
being accessed between the call to work_on
and t
's destruction.
Most of the time, the situation is not that simple. Instead, the code is apt to look more like this:
{ Thing t; do_something(t); work_on(t); do_something_else(); }
Now the seemingly simple question of whether t
is used after the call to work_on
becomes impossible to answer. The reason is that do_something
might have a Thing&
as its parameter. In that case, do_something
can save the address of t
somewhere, and do_something_else
might use that address to access t
. Moreover, it is possible for do_something
and do_something else
to be separately compiled, and until the definitions of these functions are nailed down, there is truly no way to know whether do_something_else
will access t
.
We can summarize these observations simply:
When code appears between when a variable is defined and when it is destroyed, a compiler cannot generally prove that that code will not access the variable.
The key difference between
Thing t; work_on(t);
and
work_on(Thing());
is that in the latter example, no variable is being defined. Therefore, there is no possibility of code coming between the variable's definition and its destruction. Instead, we have a nameless object of type Thing
that we know will be destroyed at the end of the expression that contains its definition. This fact is the justification for basing the decision to move rather than copy on whether what is being copied is an lvalue or rvalue. In the case of an lvalue, there might or might not be code that can access the object after it is copied; in the case of an rvalue, such code cannot exist.
Moreover, the lvalue/rvalue distinction is a local one. To see what I mean by this, look again at the very first example:
Thing t; work_on(t);
without the curly braces. Is t
used after the call to work_on
? There's no way to know without seeing the code that follows that call. So if we allow the compiler to inspect that code in order to decide whether to copy or move t
in the call to work_on
, we have the weird result that code that occurs after calling work_on
might affect the behavior of the call to work_on
itself.
All of this discussion leads to a simple rule:
When we pass an rvalue as an argument to a function, the compiler will move that rvalue instead of copying it when it is possible to do so.
Next week, we'll look more into how the C++ type system helps the compiler decide when moving an object is possible.