Concepts / Rvalue References and Move Semantics

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.

  • Programming

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?

  • Yes, it allocates new storage and copies every element

  • No, it takes over the existing storage with a fixed amount of bookkeeping

Reveal answer
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.

Definition

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.

PropertyLvalueRvalue
Rough meaningSomething that can appear on the left-hand side of an assignmentA value you cannot assign to
Typical exampleA named variableAn integer returned by a function call
Can it be assigned to?YesNo
Can an rvalue reference bind to it?Not the kind of reference this article describesYes, 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.

Code walkthrough
class Vector {
public:
    Vector(Vector&& a)
        : elem{a.elem}, sz{a.sz}
    {
        a.elem = nullptr;
        a.sz = 0;
    }
private:
    double* elem;
    int sz;
};
  1. 1
    The signature Vector(Vector&& a) declares a constructor whose argument is an rvalue reference, which is what lets the compiler select it.
  2. 2
    The 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.
  3. 3
    The first assignment in the body resets a.elem to nullptr, so a no longer refers to those elements.
  4. 4
    The second assignment in the body sets a.sz to 0, so a no longer describes any elements.
  5. 5
    Nothing 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.

aelem refers to the storage,sz is 3aelem is nullptr, sz is 0Element storage3 elementsNew objectelem and sz taken from aElement storage3 elements, now owned bynew objectelem refers toelem refers to
Which object owns the element storage before the move, and which owns it afterward? The storage stays where it is; only the references to it change.
Worked example

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?

  1. 1
    Start state

    t has elem pointing to the 3-element storage and sz equal to 3. It owns the storage.

  2. 2
    Initialise b from t

    Because t is an rvalue, the move constructor is selected. Its parameter Vector&& a binds to t.

  3. 3
    Borrow 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.

  4. 4
    Reset 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.

  5. 5
    Final 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.

Result

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.

Key point

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.

Initialised from anrvalue?for example a functionreturn valueMove constructortakes Vector&& a andtransfers resourcesCopy constructorsource is not an rvalueyesno
When the compiler sees the initialisation of a new object, does the source count as an rvalue? If so, the move constructor is chosen; otherwise the copy constructor is chosen.

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.

Owns storageelem refers to elements, szis positiveAssigned new valuetypically still allowedMoved-fromelem is nullptr, sz is 0Destroyeddestructor runs safely onthe emptied statemoveassignmentend of scope
After the move, what state is the source in, and which operations must still work on it? The source is empty but must remain safe to destroy and, typically, safe to assign to.

Mistakes Beginners Make with Moved-From Objects

  • Common mistake

    Treating a moved-from object as if it still owns the elements

    What it looks like

    After Vector b is built from t, using t as though its 3 elements are still available.

    Why it fails

    The move set t.elem to nullptr and t.sz to 0, so t no longer holds the elements.

    Do this instead

    Use 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 like

    Expecting the move constructor to allocate a second buffer and loop over the elements.

    Why it fails

    The move constructor takes over the existing storage with a fixed number of pointer and integer assignments.

    Do this instead

    Understand 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 fails

    A 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 instead

    Check 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 fails

    A move constructor does not take a const argument, because it must modify the source to remove its value.

    Do this instead

    Write 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

EASY

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.
MEDIUM

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.

More concepts starting with R