Skip to content
VLSI Mentor

C++ Part I · Module 13 · Copy and Move Semantics

Move Construction and Move Assignment

Everything needed is now in place. Chapter 13.6 established how the language tells whether anybody still owns the object behind an expression, and Chapter 13.7 gave the parameter form that binds only to the ones nobody owns — so a function can now be written that is reachable exclusively when taking an argument's resource is unobservable. Two such functions are needed, and they are the two the Rule of Three predates: a constructor that builds a new object by taking an expiring one's resource rather than duplicating it, and an assignment that does the same to a destination that already exists. Both are short, and both carry one obligation the copying versions do not have. The object they took from is still alive. It is still a fully-fledged object with a destructor that will certainly run, and if that destructor releases a resource the new object has now claimed, the result is the double free this module started with. So a move is not finished when the resource has been taken — it is finished when the source has been left in a state where its own destructor does something harmless. This chapter writes both functions, measures the state the source is left in, and measures the difference the pair makes to the container growth that cost seven copies and eleven allocations two chapters ago.

Advanced18 min readC++Part IFundamentalsMove Semantics

Part I · Module 13 · Chapter 13.8 · Move Construction and Move Assignment

1. What a Move Is

A move transfers ownership of a resource from one object to another, instead of duplicating it.

For a class holding a pointer and a size, that is three assignments and two clears — no allocation, no copying of contents, regardless of how large the resource is.

The reason it is safe is Chapter 13.7's: the function taking T&& is reachable only when nobody still owns the source.

2. The Two Signatures

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer(Buffer&& other) noexcept;              // move constructor
    Buffer& operator=(Buffer&& other) noexcept;   // move assignment

They mirror the copy pair exactly, with const T& replaced by T&&:

CopyMove
constructorBuffer(const Buffer&)Buffer(Buffer&&)
assignmentBuffer& operator=(const Buffer&)Buffer& operator=(Buffer&&)

The parameter is not const, because the whole purpose is to modify the source — Chapter 13.7 §3 measured that T&& refuses a const argument for exactly this reason.

The noexcept is not decoration and §6 is why.

3. The Move Constructor

Azvya Education Pvt. Ltd.VLSI Mentor
the move constructor, as it appears in the curriculum index
    Buffer(Buffer&& other) noexcept
      : data_{other.data_}, size_{other.size_}
    { other.data_ = nullptr; other.size_ = 0; }

Two halves, and the second is the one people forget:

Take. The initializer list copies the handle — the pointer and the size. No allocation, no element copying. For a buffer of a million integers this is two assignments.

And clear. The body sets the source's pointer to null and its size to zero.

That second half is the obligation from §1. other is still an object; it will still be destroyed; and delete[] nullptr is a well-defined no-op — Chapter 10.3 §8 measured that the replaced operator delete was not even called for a null pointer. So nulling the source makes its destructor harmless.

Without the clear, both objects would hold the same pointer and both destructors would release it — which is Chapter 13.4's picture exactly, reached by a different route.

A note on the member order. The initializer list above lists data_ before size_, so the class must declare them in that order. My first version of this class declared size_ first and produced two -Wreorder-ctor warnings — Chapter 12.8 §3's measured hazard, in my own listing. Chapter 12.8 §9's rule applies: write the list in declaration order.

4. Move Assignment

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer& operator=(Buffer&& other) noexcept
    {
        if (this == &other) return *this;      // 1. guard
        delete[] data_;                        // 2. release what we had
        data_ = other.data_;                   // 3. take
        size_ = other.size_;
        other.data_ = nullptr;                 // 4. clear the source
        other.size_ = 0;
        return *this;
    }

Four steps rather than two, because the destination already owns something — the same extra burden Chapter 13.3 §2 identified for copy assignment.

Note what is different from copy assignment: there is no allocation, so Chapter 13.3 §7's allocate-before-release ordering is unnecessary. Nothing here can fail, which is the deeper reason this function can be noexcept.

5. The Measurement

All four operations on one class:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Buffer b = a;            (copy)        COPY ctor  (allocated, now 2 allocs)
Buffer c = std::move(a); (move)        MOVE ctor  (no allocation, still 2 allocs)
  a after being moved from: size=0 data=0x0
Buffer d(1); d = b;      (copy assign) COPY assign (allocated, now 4 allocs)
d = std::move(b);        (move assign) MOVE assign (no allocation, still 4 allocs)
  b after being moved from: size=0 data=0x0
Buffer e = makeBuffer(8); (returned temporary)   -- neither ran

Three facts:

The copies allocated and the moves did not. The allocation count stayed at 2 across the move construction and at 4 across the move assignment.

The moved-from objects are empty and valid — size=0 data=0x0. Not corrupt, not partially valid: empty. Their destructors will call delete[] nullptr and do nothing. Chapter 13.9 is what you may legitimately do with such an object.

And the returned temporary ran neither. Buffer e = makeBuffer(8); produced no copy and no move, because the object was constructed directly in e — Chapter 12.10 §7 measured that elision surviving -fno-elide-constructors, and Chapter 13.12 owns it.

6. Self-Move

Section 4's move assignment opens with a self-assignment guard, and it is worth measuring what happens without one — and what the standard types do, since the answer is not the same as for copy assignment.

Measured on a std::string:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compiler: warning: explicitly moving variable of type 'std::string' to itself [-Wself-move]
before    : size=54
after self: size=0  (valid but unspecified)
reassigned: "reassigned"

Three facts.

The compiler warns — -Wself-move, for the literal s = std::move(s) form, exactly as -Wself-assign-overloaded catches the literal a = a in Chapter 13.3 §4. And with the same limitation: an indirect self-move through two references or pointers is not flagged.

The string ended up empty, not corrupt. std::string's move assignment is permitted to leave the destination in any valid state, and self-move lands within that permission — Chapter 13.9's valid but unspecified rule, applied to an object that happens to be both source and destination.

And it was still usable — assigning "reassigned" worked normally.

So self-move is not a crash, and it is not a correctness guarantee either: a self-move may lose the value. That is why the guard in §4 matters for your types, where the consequence is worse than losing a value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (this == &other) return *this;      // without this:
    delete[] data_;                        //   releases our buffer
    data_ = other.data_;                   //   ...which IS other.data_, now dangling

Without the guard, a self-move on Chapter 13.8's Buffer releases the buffer and then adopts the pointer to it — Chapter 10.7's use-after-free, set up deliberately. One line prevents it, and unlike copy assignment's guard there is no allocation to order around, so the guard is the whole fix.

Where self-move actually arises is the same place as self-assignment: not as a literal, but through two handles to one object — v[i] = std::move(v[j]) in a loop where i and j coincide, or a swap written by hand. Chapter 13.3 §4's observation applies unchanged: the literal form is warned about and the realistic form is not.

7. Why noexcept

The keyword in the signature has a measurable consequence, and it is the reason containers care.

A std::vector that needs to grow must relocate its existing elements into the new storage. If moving an element could throw partway through, the container would be left with some elements moved and some not, and no way to recover — the originals have already been gutted. So the implementation checks: if the element's move constructor is noexcept, it moves; if not, it copies, because copying can be abandoned safely (the originals are untouched).

So a move constructor that is not marked noexcept is frequently not used at all by exactly the code that benefits most from it. The mark is a promise the container relies on, and §4 explained why it is an honest promise here: taking a pointer and nulling a pointer cannot fail.

Chapter 22.6 is noexcept properly, including what happens if a noexcept function does throw. For now: mark move operations noexcept when they genuinely cannot throw, which for a handle-stealing move is always.

8. The Headline

The same container growth Chapter 13.2 §5 measured, run on two types that differ only in whether a move constructor exists. Four push_backs of a sixteen-element buffer into a vector with reserve(1):

copiesmovesallocations
copy-only type7011
type with a move constructor074

Seven copies and eleven allocations became zero copies, seven moves and four allocations.

The four allocations are exactly the four Buffer(16) objects that were constructed. The other seven were pure overhead — reallocation duplicating elements that were already correct, and then destroying the originals.

And the thing to notice about the two rows: no call site changed. The main is identical. The only difference is that one type offered a function for the rvalue case, and Chapter 13.6's classification routed the calls to it. Chapter 13.7 §4's point, measured: the fast path is taken automatically.

9. Both, or Neither

The move pair has the same consistency requirement as the copy pair, and Chapter 13.10 is the full rule. The short version:

Write both move operations or neither. A type with a move constructor and no move assignment will copy on assignment, which is correct and surprising.

And the five functions interact. Chapter 13.10 measures this, and one result is worth knowing before you write a destructor: declaring a destructor suppresses the generated move operations entirely, so a class that follows Chapter 13.5's Rule of Three and stops there has quietly lost the ability to move — and falls back to copying everywhere, which §7 measured as 7 copies instead of 0.

10. What to Actually Do

Write a move constructor and move assignment for any type that owns a raw resource — or neither, if the type is not movable.

Take T&&, not const T&& — you must modify the source.

Take the handle, then clear the source. The clear is the half that prevents a double free.

Mark both noexcept. Containers check, and copy instead if it is absent (§6).

Write the initializer list in declaration order — I triggered -Wreorder-ctor writing this chapter's own class.

Guard move assignment against self-assignment, release the old resource first, then take and clear.

Do not add move operations to a value type — the generated ones are already optimal.

Expect no allocation in a move — that is how you verify one is being used.

And know that a destructor suppresses the generated moves (Chapter 13.10).

11. Mistakes Worth Making Once

Forgetting to clear the source. Both objects hold one pointer; Chapter 13.4's picture and its double free.

Omitting noexcept. Containers will copy instead, so the optimisation silently does not happen.

Taking const T&&. You cannot modify the source, so you cannot steal from it.

Writing a move constructor and no move assignment. Assignment then copies.

Listing members out of declaration order. -Wreorder-ctor — measured on my own first version.

Forgetting the self-assignment guard in move assignment. delete[] data_ then taking from yourself.

Adding moves to a type holding only scalars. No benefit; the generated ones already do the right thing.

Expecting std::move to be the optimisation. It enables one (Chapter 13.9).

Following the Rule of Three and stopping. The destructor suppressed your moves (Chapter 13.10).

12. Interview Relevance

"Write a move constructor." Take the handle in the initializer list, then null the source in the body, and mark it noexcept. Candidates who omit the clear have written the double free; candidates who omit noexcept have written something containers will not use.

"Why must the source be left valid?" Because its destructor still runs. The strong answer names the specific consequence — a non-null source pointer means two releases of one buffer.

"Why noexcept?" Because std::vector relocating elements cannot recover from a throw halfway through, so it checks and falls back to copying if the move can throw. This is the detail that distinguishes real familiarity.

"How much does a move save?" It is O(1) against O(n) — not a constant factor. The measured 7 copies / 11 allocations versus 0 copies / 4 allocations is a good concrete answer.

"Is a move always cheaper?" No — for a type holding only scalars, a move is a copy, and that is correct.

"What is left in a moved-from object?" Measured here as empty and valid — size 0, pointer null. Chapter 13.9's subject.

13. What You Should Be Able to Say Now

  1. A move transfers ownership rather than duplicating — for a handle type, O(1) regardless of size.
  2. It is safe because T&& is reachable only when nobody owns the source (Chapter 13.7 §4).
  3. Two signatures mirroring the copy pair, with T&& and no const.
  4. Two halves: take the handle, then clear the source. The clear prevents Chapter 13.4's double free.
  5. delete[] nullptr is a well-defined no-op (Chapter 10.3 §8), which is what makes the cleared source destructible.
  6. Move assignment has four steps — guard, release, take, clear — and cannot fail, so no allocate-first ordering is needed.
  7. Measured: copies allocated; moves did not — the count stayed at 2 and then 4.
  8. Measured moved-from state: size=0 data=0x0 — empty and valid.
  9. Measured: a returned temporary ran neither copy nor move (Chapter 13.12).
  10. noexcept matters: a container that cannot rely on it copies instead, so the optimisation silently vanishes.
  11. Measured headline: 4 insertions of a 16-int buffer — 7 copies / 11 allocations without a move constructor, 0 copies / 7 moves / 4 allocations with one.
  12. The 4 allocations are the 4 objects; the other 7 were pure overhead.
  13. No call site changed between the two rows.
  14. Write both move operations or neither, and a destructor suppresses the generated ones (Chapter 13.10).

14. Exercises

Exercise 1 — write both. Give the move constructor and move assignment for a class holding char* text_ and int length_, marking the step that prevents a double free.

Exercise 2 — find the bug. Buffer(Buffer&& o) noexcept : data_{o.data_}, size_{o.size_} {} — say what is missing and trace what happens at the end of the enclosing scope.

Exercise 3 — predict. For a class with both copy and move pairs, say which runs: B b = a;, B c = std::move(a);, d = b;, d = std::move(b);, B e = makeB();

Exercise 4 — the noexcept consequence. A colleague's move constructor is correct but unmarked, and their vector insertion is still slow. Explain why and give the one-word fix.

Exercise 5 — order of steps. Explain why move assignment does not need copy assignment's allocate-before-release ordering.

Exercise 6 — when not to. Say why a struct Point { double x, y; }; should have no move operations.

Exercise 7 — the arithmetic. For 8 push_backs of a 1 MB buffer with reserve(1), estimate bytes copied with and without a move constructor.

Exercise 8 — the suppression. A class has a destructor, a deep copy constructor and copy assignment, and no move operations. Say what happens on v.push_back(std::move(b)) and which chapter explains it.

15. Summary

  • A move transfers ownership instead of duplicating — O(1) rather than O(n).
  • Safe because T&& binds only to expressions nobody owns.
  • Two halves: take the handle, clear the source — the clear is what prevents the double free.
  • Move assignment adds a guard and a release, and cannot fail.
  • Measured: moves performed no allocation; moved-from objects were size=0 data=0x0 — empty and valid.
  • Mark both noexcept — a container that cannot rely on it copies instead.
  • Measured headline: 7 copies / 11 allocations → 0 copies / 7 moves / 4 allocations, with no call site changed.
  • A move is not always cheaper — for a scalar type it is a copy, correctly.
  • Write both or neither, and remember a destructor suppresses the generated moves.

The pair is written and one of its parts has been used throughout this module without explanation. Every measurement that produced a move rather than a copy did so because something wrote std::move, and the name is the most misleading in the standard library: it does not move anything. Chapter 13.6 measured that it returns an expression of rvalue category referring to the same object, which is a cast and nothing more, and the measurement to come makes that concrete — calling it and discarding the result leaves the object completely unchanged. The next chapter is what it does, what it is for, and the question this chapter deferred twice: given an object that something has moved from, now holding nothing, what are you still allowed to do with it.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.