Concepts / Copy Constructors and Copy Assignment

Copy Constructors and Copy Assignment

A move constructor transfers ownership of an object's resources to a new object without copying them.

  • Programming

Why Handing Over a Vector Can Be Expensive

Imagine a Vector class that owns a block of elements. Every time a Vector is handed from one place in a program to another, a naive design would duplicate the whole block, so the program pays to copy every element again and again. The cost grows with the number of elements, even though the program only wanted the value to end up in a different place.

Move semantics addresses this problem. Instead of creating a duplicate, an object can hand over the resources it already holds. The move constructor is the special constructor that makes this possible: it builds a new object by taking over another object's resources. This article focuses on that constructor and on the rvalue reference that lets the compiler choose it.

What do you think happens?

If a Vector a owns 3 elements and we construct a new Vector b by moving from a, how many elements must be copied?

  • All 3 elements are copied

  • None of the elements are copied

  • Only the first element is copied

Reveal answer
Answer:

None of the elements are copied

The move constructor does not allocate new storage and does not loop over the elements. It only reassigns which object refers to the existing storage, so the cost is a fixed number of pointer and integer assignments regardless of how many elements exist.

What the Double Ampersand Means

A move constructor's parameter is written as Vector&& a. The double ampersand declares an rvalue reference. An rvalue reference is the kind of reference that binds to rvalues, which are the temporary or otherwise short-lived values a program produces while computing, such as the result of an expression that is about to be thrown away.

Definition

An rvalue reference, written with a double ampersand such as Vector&&, is a reference that binds to a temporary or short-lived value whose resources are safe to take.

Because the move constructor's parameter is an rvalue reference, the compiler can select this constructor when it is given a value whose resources can be taken. The choice is made by the compiler based on what kind of value is being passed, so the programmer does not have to call the move constructor by name. The name of the parameter, a, is the object whose resources are being taken.

The source gives one clear case where this selection happens automatically: when a function returns a value, the compiler chooses the move constructor to transfer that return value out of the function. For this reason, an expression such as r = x + y + z moves Vector objects rather than copying them.

Tracing a Move Through the Constructor

The following sketch shows the shape of a move constructor for a Vector that stores a pointer to its elements, elem, and a count, sz. The member types and the surrounding class are simplified here for illustration; the constructor's signature and its two body steps follow the description in the source.

python
Walk through it
  1. 1
    The parameter Vector&& a binds to a value whose resources can be taken, and a names that source object.
  2. 2
    The member initializer list copies the pointer elem from a into the new object. Only the address stored in the pointer is copied, not the elements it points to.
  3. 3
    The member initializer list copies the count sz from a into the new object.
  4. 4
    The first body line sets a.elem to nullptr, so the source no longer refers to the storage.
  5. 5
    The second body line sets a.sz to 0, so the source describes an empty Vector.
Notes on the code
  • Vector&& a declares an rvalue reference parameter named a
  • elem{a.elem} makes the new object's element pointer refer to the same storage as a
  • sz{a.sz} copies the count, which is a single integer
  • a.elem = nullptr removes a's reference to the storage
  • a.sz = 0 records that a 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 cost of the operation is a fixed number of pointer and integer assignments, no matter how many elements the Vector holds. This is why the source describes the constructor as trivial to define for Vector: the work is bookkeeping about which object owns the storage.

Before
Vector a;   // a owns the element storage, sz = 3
Vector b(std::move(a));

a owns the storage and records 3 elements

After
// b now owns the element storage, sz = 3
// a holds nullptr and sz = 0

b owns the storage and records 3 elements; a is empty

Who Owns the Storage Before and After

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 itself does not move in memory. What changes is which object's member refers to it and which object's count describes it. The diagram below shows this transfer using an illustrative example with three elements; the three-element count is chosen for the example and is not a value taken from the source.

aelem and sz = 3aelem = nullptr, sz = 0element storage3 elementsnew object belem and sz = 3element storage3 elementselem points toelem points to
Where does the storage live before the move, and after the move which object's elem points to it?

Read the diagram as a question of ownership. The storage is the same block in both halves. Before the move, the arrow from a leads to it. After the move, the arrow comes from the new object, and a no longer points anywhere.

The Source Object After a Move

The source object is not destroyed by a move. It remains a valid object that the program can still refer to, but the source material says that its elem is set to nullptr and its sz is set to 0. After that, it holds no elements and must not be used as the owner of them. The state change happens in two steps, and the diagram below follows the source object through them.

a holds storageelem set, sz = 3constructor body runsa.elem = nullptr; a.sz = 0a is emptyelem = nullptr, sz = 0movereset
After the move constructor runs, what state is the source object in, and how does its pointer differ from before the move?

Copy Versus Move Side by Side

A copy constructor builds a new object with its own copy of the resource, so both objects end up with separate storage. A move constructor builds the new object by reassigning the existing resource, so only one object owns it at a time. The diagram below sets the two operations side by side.

allocate new storageborrow elem pointercopy every elementcost grows with sizeborrow szone integertwo separate blocksreset sourcenullptr and 0
What is duplicated and allocated in a copy, compared with what is simply reassigned in a move?

The contrast is about where the cost is paid. The copy path does work proportional to the number of elements. The move path does a fixed amount of bookkeeping. This is the reason the source treats move semantics as the answer to repeatedly duplicating a Vector as it moves through a program.

Mistakes With Moved-From Objects

  • Common mistake

    Assuming both objects still own the elements after a move

    What it looks like

    After Vector b(std::move(a)), treating a as if it still refers to the storage

    Why it fails

    The constructor sets a.elem to nullptr and a.sz to 0, so the storage now belongs to b.

    Do this instead

    Use b as the owner of the elements. Treat a as holding no elements.

  • Common mistake

    Believing a move copies the elements

    What it looks like

    Expecting the time to grow with the number of elements in the Vector

    Why it fails

    The move constructor does not allocate storage or loop over elements; it performs a fixed number of assignments.

    Do this instead

    Think of a move as reassigning ownership of existing storage.

  • Common mistake

    Thinking the parameter name determines which object is moved from

    What it looks like

    Confusing the name a with the new object

    Why it fails

    In the constructor, a is the source whose resources are taken; the new object is the one being built.

    Do this instead

    Read the body line by line and ask which object each assignment changes.

Scope Note on Move Assignment

Predicting Ownership in a Short Trace

EASY

A Vector a owns storage and records 3 elements. The program then runs Vector b(std::move(a)). Without looking back, state the value of b.elem, the value of a.elem, the value of b.sz, and the value of a.sz. Then explain which object is safe to use as the owner of the 3 elements.

Hints
  • Start with the pointer and count that b receives from a in the member initializer list.
  • Then check which two assignments the body makes to a.
MEDIUM

Explain, in your own words, why a move constructor's cost does not depend on how many elements the Vector holds, and name the two lines in the body that make the source object empty.

Hints
  • Consider whether the constructor allocates storage or loops over elements.
  • The two body lines reset the pointer and the count of the source.

Key Takeaways

  • A move constructor transfers ownership of an object's resources to a new object without copying them.
  • Its parameter Vector&& a is an rvalue reference, which lets the compiler select the move constructor when a value's resources can be taken, such as a function's return value.
  • The new object borrows the element pointer and size from the source, and the source is reset to nullptr and 0, so it holds no elements.
  • The cost of a move is a fixed number of pointer and integer assignments, not a duplication of every element.
  • Move assignment is named in this chapter's title but is not covered by the source excerpt used for this article.

More concepts starting with C