How the C++ Compiler Decides to Move Objects
std::move
does, although not necessarily how it does it.
Two weeks ago, I discussed rvalue references. I noted that, as its name suggests, an rvalue reference is a reference that is bound to an rvalue. More specifically, an rvalue reference can be bound only to an rvalue:
int n = 3; int& r1 = 3; // Error, 3 is an rvalue int& r2 = n; // OK int&& r3 = 3; // OK int&& r4 = n; // Error, n is an lvalue
If we add const
to the equation, both a plain reference and an rvalue reference can bind to an rvalue:
const int&& r5 = 3; // OK
However this binding involves adding a const
to the type of 3
. This addition turns out to be important for the following subtle reason:
void foo(const int&); void foo(int&&);
Suppose we overload a function to accept a plain reference to const
in one version and an rvalue reference in the other. Then whenever we call foo
with an lvalue, we will get foo(const int&)
; whenever we call foo
with an rvalue, we will get foo(int&&)
. The const
is necessary for the plain reference because otherwise we would not be able to give foo
a const int
lvalue as its argument.
Inside the body of foo(const int&)
, its parameter is a reference to const
. As usual, the argument is not copied, and the function itself cannot change the parameter's value because the parameter is const
. The interesting part is what happens inside the body of foo(int&&)
.
Because an rvalue reference is just another kind of reference, the parameter of foo(int&&)
is a reference. This reference is not a reference to const
. In other words, the function is permitted to change its parameter's value, even though this value is an rvalue. For example:
void foo(int&& n) { ++n; }
This function is permitted to increment n
because n
is a reference. It is permitted to do so even though we can call
foo(3);
Here, 3
is an rvalue, so n
gets bound to 3
without 3
being copied. Nevertheless, the function is permitted to change the value of n
. Does doing so change the value of 3
? The answer is that we can't tell, because there is no way of inspecting the value of 3
later on to discover whether it changed!
Now let's see how to apply this technique to defining a constructor:
class Thing { public: // … Thing(const Thing& t) { /* … */ } Thing(Thing&& t) { /* … */ } // … };
We have defined a class Thing
with two overloaded constructors. The first of them takes a reference to const Thing
. Inside the body of this constructor, t
refers directly to the object from which we want to construct our new Thing
. As ever, we can use the contents of this object, but we cannot change those contents.
In the second constructor, t
also refers to an object from which we are to construct a Thing
. In this constructor, however, that object is known to be an rvalue — which means that we can change the contents of t
without worrying about clobbering important data.
When we overload constructors in this way, we generally refer to the first one as the copy constructor and the second as the move constructor. Similarly, we can have copy-assignment and move-assignment operators as well:
class Thing { public: // … Thing& operator=(const Thing& t) { /* … */ } Thing& operator=(Thing&& t) { /* … */ } // … };
For both constructors and assignment operators, the copy version is required to leave the original alone; the move version is permitted to destroy the original's value so long as the original object is left in a state that allows it to be safely destroyed.
Having seen this technique, you should now be able to figure out what std::move
does, although not necessarily how it does it. Try to do so before we discuss it in detail next week.