Rvalue References and Move Semantics
The move constructor's argument is non-const because the transfer must modify the source. Resetting elem to nullptr and sz to 0 is only possible if the source can be changed.
The Cost of Handing Over a Large Value
Imagine a Vector class that owns a block of elements. Each time a Vector is handed from one place in a program to another, a naive design would duplicate it, and the program would pay for copying every element again and again. For a large Vector this cost can dominate the running time of the whole program.
Consider a function that builds a large result inside itself and then hands that result back to the caller. Copying every element of that result is wasted effort, because the function is finished with its local object the moment it returns. The question this article answers is whether the program can take the value directly from the temporary object and leave the temporary empty. C++ provides a mechanism for exactly this kind of transfer, and it starts with the double ampersand notation, &&.
What do you think happens?
When a move constructor builds a new Vector from an existing one, does it allocate new storage and copy each element?
Reveal answer
No, it takes over the existing storage with a fixed amount of bookkeeping
The move constructor does not allocate new storage and does not loop over the elements. Its cost is a fixed number of pointer and integer assignments, no matter how many elements the Vector holds.
Lvalues, Rvalues, and the && Notation
The word rvalue is understood against its partner, lvalue. An lvalue is roughly something that can appear on the left-hand side of an assignment. An rvalue, to a first approximation, is a value you cannot assign to. An integer returned by a function call is a typical example: you can use it, store it, or pass it along, but you cannot assign to it the way you assign to a named variable.
An rvalue reference, declared with &&, is a reference to something nobody else can assign to. Because no other part of the program can assign to it, no other part of the program is depending on it being left unchanged, so its value can safely be taken.
| Property | Lvalue | Rvalue |
|---|---|---|
| Rough meaning | Something that can appear on the left-hand side of an assignment | A value you cannot assign to |
| Typical example | A named variable | An integer returned by a function call |
| Can it be assigned to? | Yes | No |
| Can an rvalue reference bind to it? | Not the kind of reference this article describes | Yes, through an rvalue reference declared with && |
The key distinction is whether the value is something other parts of the program may still be relying on.
Why an Rvalue's Value Can Be Stolen
The phrase steal its value is the heart of the idea, and stealing here does not mean wrongdoing. The receiving object takes over the contents of the temporary, because the temporary is about to disappear and no one else needs those contents. The program recognises that the value has no remaining owner who cares about it, so ownership can simply pass to the new holder.
The source gives a concrete case of this pattern: the local variable res inside the addition operator for a Vector class. That variable is built inside the function. At the moment it is returned, it is a value nobody else can assign to, so it is a natural candidate for having its value stolen rather than copied.
The rule is about how the value is being used at that point in the program, not merely about the type of the variable. The same object could be copied in one statement and moved in another, depending on whether the source is an rvalue at that moment.
Reading the Move Constructor Line by Line
A move constructor is a special constructor that takes an rvalue reference and transfers ownership of the object's resources to the object being constructed. Its parameter is written Vector&& a. The name a is the object whose resources are being taken. The sketch below shows only the parts this article discusses; a complete class contains other members that are not shown here.
class Vector {
public:
Vector(Vector&& a)
: elem{a.elem}, sz{a.sz}
{
a.elem = nullptr;
a.sz = 0;
}
private:
double* elem;
int sz;
};- 1The signature
Vector(Vector&& a)declares a constructor whose argument is an rvalue reference, which is what lets the compiler select it. - 2The member initialiser list borrows the element pointer and the size from a. After this line, the new object refers to the same elements as a.
- 3The first assignment in the body resets a.elem to nullptr, so a no longer refers to those elements.
- 4The second assignment in the body sets a.sz to 0, so a no longer describes any elements.
- 5Nothing in the body allocates storage or loops over the elements. The work is a fixed number of pointer and integer assignments, regardless of how many elements the Vector holds.
- Vector&& a is an rvalue reference parameter; the double ampersand makes it bind to rvalues
- elem{a.elem} copies the element pointer from a into the new object
- sz{a.sz} copies the element count from a into the new object
- a.elem = nullptr; makes the source stop referring to the elements
- a.sz = 0; records that the source now holds no elements
Notice what the constructor does not do. It does not allocate new storage and does not loop over the elements to copy them. The source introduces this constructor as easy to define for Vector because the work is only bookkeeping about which object owns the storage.
Ownership Before and After a Move
Before the move, the source object a owns the element storage and records how many elements it holds. After the move, the storage belongs to the new object, and a is left in a deliberately empty state. The element buffer does not move in memory. What changes is which object's member refers to it and which object's count describes it.
Tracing a temporary handed to a new object
A temporary Vector t holds 3 elements in its storage. A new Vector b is initialised from t, and the move constructor runs. Who owns the storage afterward, and what state is t in?
- 1Start state
t has elem pointing to the 3-element storage and sz equal to 3. It owns the storage.
- 2Initialise b from t
Because t is an rvalue, the move constructor is selected. Its parameter Vector&& a binds to t.
- 3Borrow the resources
b.elem is set to t.elem and b.sz is set to t.sz. Now b refers to the same storage and describes the same 3 elements.
- 4Reset the source
t.elem is set to nullptr and t.sz is set to 0. t no longer refers to the elements and holds none.
- 5Final ownership
b owns the storage. t is a moved-from object with no elements, and it must not be used as the owner of them.
b owns the 3-element storage. t has elem set to nullptr and sz set to 0.
Why the Move Constructor Takes a Non-const Argument
A move constructor does not take a const argument. The reason follows from its purpose: a move constructor is supposed to remove the value from its argument. A const argument could not be modified, so the constructor could never empty it. A move assignment is defined in a similar way, so the same reasoning applies to it.
The move constructor's argument is non-const because the transfer must modify the source. Resetting elem to nullptr and sz to 0 is only possible if the source can be changed.
When the Compiler Selects the Move Constructor
A move operation is applied when an rvalue reference is used as an initializer or as the right-hand side of an assignment. The trigger is a specific situation: an object is being initialised from an rvalue, or assigned from one. The move is not something you request by naming a function in the middle of an expression. It is selected because the value on the right is one that can be stolen.
The source gives an example from the chapter: the expression r = x + y + z. The compiler chooses the move constructor to transfer the return value out of each addition, so the intermediate Vector objects are moved rather than copied.
The Moved-From Object and Its Destructor
After a move, the source object is not discarded in an unusable condition. The source object will still be destroyed at the end of its scope, and its destructor must be able to run safely on whatever state the move has left. This is a firm requirement. The destructor runs on an emptied source, and that is safe because the source no longer claims the storage.
The source also sets a further expectation. Typically, assignment to a moved-from object should still be allowed. After its value has been taken, the object can be given a new value. The object is empty in the sense that its old value has gone, but it is still a working object that can be reused. It should not be relied on to hold a meaningful value until it is given one.
Mistakes Beginners Make with Moved-From Objects
Common mistake Treating a moved-from object as if it still owns the elements
What it looks likeAfter Vector b is built from t, using t as though its 3 elements are still available.
Why it failsThe move set t.elem to nullptr and t.sz to 0, so t no longer holds the elements.
Do this insteadUse b as the owner of the elements. Treat t as empty and do not rely on it to hold a meaningful value.
Common mistake Assuming the move copies the elements
What it looks likeExpecting the move constructor to allocate a second buffer and loop over the elements.
Why it failsThe move constructor takes over the existing storage with a fixed number of pointer and integer assignments.
Do this insteadUnderstand the cost of the move as bookkeeping about which object owns the storage.
Common mistake Expecting a move to happen from a named variable on its own
Why it failsA move operation is applied when an rvalue reference is used as an initializer or as the right-hand side of an assignment. The rule depends on how the value is used at that point.
Do this insteadCheck whether the value on the right is an rvalue in that statement before expecting the move constructor to be selected.
Common mistake Writing the move constructor parameter as const Vector&&
Why it failsA move constructor does not take a const argument, because it must modify the source to remove its value.
Do this insteadWrite the parameter as Vector&& a so the constructor can reset the source.
Common mistake Forgetting that the source's destructor still runs after the move
Practice: Tracing Ownership in a Move
A temporary Vector t holds a block of storage with 4 elements. A new Vector b is initialised from t, and the move constructor runs. Predict: which object owns the storage, what are t.elem and t.sz afterward, and what must still work on t before it is destroyed?
Hints
- Ask which object's elem now refers to the storage after the constructor body runs.
- Recall the two assignments in the body: one resets the pointer and one resets the count.
- Think about what the destructor does at the end of t's scope, and what assignment to a moved-from object typically requires.
Explain in your own words why the move constructor's argument must not be const, and identify the two lines in its body that make the source empty.
Hints
- The source must be modified for its value to be removed.
- The two lines are the assignment of nullptr to elem and the assignment of 0 to sz.
Key Takeaways
- A move constructor takes an rvalue reference, Vector&& a, and transfers ownership of an object's resources to the new object without copying them.
- The move constructor does a fixed amount of bookkeeping: it borrows the element pointer and size, then resets the source to elem nullptr and sz 0.
- The compiler selects the move constructor when the value being used is an rvalue, such as a function return value, in initialisation or the right-hand side of an assignment.
- A moved-from object holds no elements, its destructor must still run safely, and it should typically still allow assignment, but its value should not be relied on.