Skip to content
VLSI Mentor

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

The Rule of Five

This module has now written all five of the functions a resource-owning class may need and treated each as an independent decision. They are not independent. Declaring any one of them changes which of the others the compiler will generate, the rules are not symmetric, and they are not guessable from first principles — which would be a minor annoyance except that the most common case has a silent cost. A class written exactly to the Rule of Three, with a destructor and both copy operations and nothing else, has no move operations at all, because declaring a destructor suppresses them. Nothing is reported. The class works correctly and copies in every situation where it could have transferred, which Chapter 13.8 measured as seven copies and eleven allocations where zero copies and four allocations were available. Establishing that took more care than expected, because the obvious way to ask the question — the standard library's type traits — gives an answer that is true and misleading: it reports such a class as move-constructible, which it is, by means of its copy constructor. Counting which function actually runs gives the opposite result, and the gap between those two measurements is worth as much as the rule itself.

Advanced18 min readC++Part IFundamentalsMove Semantics

Part I · Module 13 · Chapter 13.10 · The Rule of Five

1. The Five

#FunctionChapter
1destructor12.7
2copy constructor13.2
3copy assignment13.3
4move constructor13.8
5move assignment13.8

The Rule of Five: if you need to write any of these, consider all five, and write them consistently.

It is the Rule of Three updated for C++11. Chapter 13.5 §8 noted that the older rule counts to three because it predates the move operations, and that its reasoning — all of them answer what does this object own? — carries over unchanged.

2. The Interactions Are Not Symmetric

What makes five harder than three is that declaring one of them affects whether others are generated, and the rules differ depending on which one you declare.

The compiler's behaviour, stated plainly:

If you declare……then the compiler does not generate
a destructorthe move constructor and move assignment
a copy constructorthe move constructor and move assignment
copy assignmentthe move constructor and move assignment
a move constructorthe copy constructor and copy assignment (deleted)
move assignmentthe copy constructor and copy assignment (deleted)

Two asymmetries worth noticing:

Anything you declare suppresses the moves. Including a destructor, which is the case that bites.

Declaring a move operation deletes the copies rather than merely not generating them — so copying becomes a compile error rather than falling back to something.

3. The Copy Side, Measured

Declaring a move constructor and then copying:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct M { M() = default; M(M&&) noexcept {} int x; };
    M a;
    M b = a;        // ?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: call to implicitly-deleted copy constructor of 'M'
note: copy constructor is implicitly deleted because 'M' has a user-declared move constructor

A compile error, with the reason named. This direction is loud and therefore harmless: you find out immediately.

Chapter 13.1 §8 measured the same rule arriving three notes deep in a different diagnostic — a struct holding a std::unique_ptr could not be copied because unique_ptr has a user-declared move constructor. That error chain was this rule applied inside the standard library.

4. The Move Side, Measured — and the Trap

The other direction is silent, and measuring it correctly took two attempts.

The first attempt, using type traits:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                 copy-ctor  copy-assign  move-ctor  move-assign
Nothing              1           1           1           1
HasDtor              1           1           1           1      <-- ?
HasCopyCtor          1           1           1           1      <-- ?
HasMoveCtor          0           0           1           0

Read naively, the HasDtor row says a user destructor suppresses nothing — move-ctor is still 1. That conclusion is wrong, and the trait is not lying.

std::is_move_constructible<T> asks: can a T be constructed from an rvalue? And a copy constructor can do that — const T& binds to an rvalue, as Chapter 13.7 §3 measured. So the trait is true whenever construction from an rvalue is possible by any means, including copying.

The second attempt, counting which function actually runs. A member that reports whether it was copied or moved, and then constructing from std::move(src):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  Nothing    constructing from an rvalue ran: copy=0 move=1  -> a MOVE ctor exists
  HasDtor    constructing from an rvalue ran: copy=1 move=0  -> NO move ctor; fell back to COPY
  HasCopy    constructing from an rvalue ran: copy=1 move=0  -> NO move ctor; fell back to COPY

The opposite answer. A user-declared destructor does suppress the move constructor, and the class silently copies instead.

Note which parts of the trait table were reliable: the HasMoveCtor row's zeros are real — a 0 means genuinely deleted, which §3 confirmed with a compile error. It is the 1s in the move-ctor column that cannot be trusted.

5. The Method Lesson

This is worth separating out, because it has now happened four times in this curriculum.

A probe that answers a broader question than the one you asked will answer it confidently and uselessly.

is_move_constructible answers "can this be built from an rvalue". The question was "does this have a move constructor". Those differ exactly when a fallback exists — and a fallback existing is the entire phenomenon being investigated.

The reliable instrument is the one Chapter 13.2 used: make each candidate function announce itself and count. That technique has now settled four questions in this module that reasoning or traits got wrong or could not reach.

The three earlier instances, for the pattern: Chapter 10.3 §5's new int reading 0 on every attempt without that being a guarantee; Chapter 11.4 §7's identical access-section offsets with the compiler warning that the type was non-standard-layout; and Chapter 12.10 §7's elision surviving -fno-elide-constructors, where the flag that should have revealed a copy could not.

6. Why a Destructor Suppresses the Moves

The rule sounds arbitrary and is not. The reasoning is the Rule of Three's, run forward:

You declared a destructor, so the object owns something (Chapter 12.7's callout).

So a generated move would be wrong. A generated move is memberwise, exactly like a generated copy — it would take the handle and leave the source holding it too, because nothing would clear it. That is Chapter 13.4's two-objects-one-buffer picture, with the double free that follows.

So the compiler declines to generate one. Rather than produce a move that is wrong for precisely the types that have destructors, it generates nothing and lets the copy handle rvalues — which is slower and correct.

That is a defensible default: silently slow beats silently wrong. The cost of the default is §7.

7. What the Suppression Costs

Chapter 13.8 §7's measurement, re-read as the price of stopping at the Rule of Three:

copiesmovesallocations
no move constructor7011
with a move constructor074

A class that follows the Rule of Three and stops there is the first row. It is correct, nothing is reported, and it does roughly twice the allocation work in any container.

And Chapter 13.2 §6's arithmetic applies: without the ability to move, container insertion costs 2n − 1 copies for n elements rather than n moves.

So the practical rule:

If you declared a destructor, you have already suppressed your move operations. Declare them.

8. Writing All Five Consistently

For a resource-owning type, two shapes are coherent.

The full set — the type behaves like a value and transfers cheaply:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    ~Buffer();                                        // release
    Buffer(const Buffer&);                            // deep copy
    Buffer& operator=(const Buffer&);                 // guard, allocate, copy, release
    Buffer(Buffer&&) noexcept;                        // take, clear
    Buffer& operator=(Buffer&&) noexcept;             // guard, release, take, clear

Chapter 13.2 §2, Chapter 13.3 §7 and Chapter 13.8 §3–§4 are the four bodies; Chapter 12.7 is the destructor.

Or move-only — the type represents a unique claim:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    ~Handle();
    Handle(const Handle&)            = delete;
    Handle& operator=(const Handle&) = delete;
    Handle(Handle&&) noexcept;
    Handle& operator=(Handle&&) noexcept;

This is what Chapter 12.9 §9 called the honest-but-restricted shape, now un-restricted: std::unique_ptr is exactly this, which is why Chapter 13.1 §8's measurement showed its copy deleted and Chapter 14.2 is where it is treated properly. A move-only type can be returned from a factory and stored in a container, which the copy-deleted-only version could not.

Two things to do in either shape.

Mark the moves noexcept. Chapter 13.8 §6 measured why — a container that cannot rely on it copies instead, silently undoing the work.

And use = default where the compiler's version is right, which Chapter 12.4 §5 measured as preserving triviality where an empty body does not. Writing ~Buffer() = default; alongside hand-written copies documents the decision without changing behaviour.

9. Five Is Not the Target

The rule says consider all five, and this module's conclusion is that the right number to write is usually none.

Every measurement here has been on a class holding a raw int*. Replace that with a std::vector<int> and the compiler generates all five correctly: the destructor releases the vector, the copies duplicate it, and the moves transfer it — because std::vector has its own correct five, and the generated memberwise versions delegate to them. Chapter 13.1 §3 measured that delegation for std::string.

And note the suppression rules then work for you rather than against: with no user-declared destructor, nothing is suppressed, so all five are generated.

Chapter 13.11 is that argument in full, and it is the module's destination rather than an epilogue.

10. What to Actually Do

Ask whether the class directly owns a raw resource. That one question decides everything.

If it does not, declare none of the five — not even = default ones.

If it does, declare all five, with = delete counting as a declaration.

Remember a destructor suppresses the moves — so declaring one obliges you to declare them.

Mark both moves noexcept or containers will copy (Chapter 13.8 §6).

Use = default where the compiler's version is correct, rather than an empty body (Chapter 12.4 §5).

Prefer move-only to copy-deleted for a unique claim — it can be returned and stored.

Do not trust is_move_constructible to tell you whether a move constructor exists. Count which function runs.

And treat "hand-written destructor, no moves" as a review finding.

11. Mistakes Worth Making Once

Following the Rule of Three and stopping. Measured: the moves are suppressed and the class copies — 7 copies / 11 allocations instead of 0 and 4.

Expecting a diagnostic for that. There is none; the fallback is correct, just slow.

Using is_move_constructible to check for a move constructor. Measured: it reported 1 for a class that had none, because copying satisfies it.

Declaring a move constructor and expecting copies to still work. Measured: error: call to implicitly-deleted copy constructor.

Declaring a subset of the five. Every subset leaves a surprising gap.

Omitting noexcept on the moves. Containers copy instead.

Writing ~T() {} instead of ~T() = default;. Costs triviality (Chapter 12.4 §5).

Declaring the five on a type that owns nothing. You have suppressed your own moves for no reason.

12. Interview Relevance

"What is the Rule of Five?" If you need any of destructor, copy constructor, copy assignment, move constructor, move assignment, consider all five. The better answer adds why — all five answer what the object owns — and that declaring one changes which others exist.

"What does declaring a destructor do to the move operations?" Suppresses them, so the class copies where it could move. This is the single most valuable fact in the chapter and it is frequently unknown.

"And declaring a move constructor?" Deletes the copy operations — a compile error rather than a fallback.

"Why does a destructor suppress the moves?" Because a generated move would be memberwise, so it would take the handle without clearing the source — the double free. Silently slow beats silently wrong.

"How would you check whether a type has a move constructor?" Count which function runs, with an instrumented member. is_move_constructible answers a broader question and reports true when only copying is available — a genuinely discriminating answer.

"How many of the five should a typical class have?" None. The Rule of Zero.

13. What You Should Be Able to Say Now

  1. The five: destructor, copy constructor, copy assignment, move constructor, move assignment.
  2. Declaring any one changes which others are generated, asymmetrically.
  3. Declaring a destructor, copy constructor or copy assignment suppresses both moves.
  4. Declaring either move deletes both copies.
  5. Measured, copy side: error: call to implicitly-deleted copy constructor … because 'M' has a user-declared move constructor — loud and harmless.
  6. Measured, move side, by counting: Nothing ran a move; HasDtor and HasCopy ran a COPY — no move constructor exists.
  7. The trait probe was misleading: is_move_constructible reported 1 for those classes, because a copy constructor satisfies construction from an rvalue.
  8. Its zeros were reliable; its ones were not.
  9. Method lesson: a probe answering a broader question answers it confidently and uselessly — count which function runs.
  10. Why the suppression: a generated move would be memberwise, taking the handle without clearing the source — the double free. Silently slow beats silently wrong.
  11. Measured cost of stopping at three: 7 copies / 11 allocations against 0 copies / 7 moves / 4 allocations.
  12. So: if you declared a destructor, declare the moves.
  13. Two coherent shapes: the full five, or move-only (copies deleted) — which std::unique_ptr is.
  14. = delete counts as declaring one.
  15. = default is the right way to take the compiler's version where it is correct, because an empty body costs triviality (Chapter 12.4 §5).
  16. The structural tell: a hand-written destructor with no move operations is almost certainly copying where it could move.
  17. The right number is usually zero (Chapter 13.11).

14. Exercises

Exercise 1 — the table. From memory, say what declaring each of the five suppresses or deletes.

Exercise 2 — predict. struct A { ~A() {} int* p; }; — say whether A b = std::move(a); moves or copies, and whether anything is reported.

Exercise 3 — the trap. Explain why is_move_constructible<A> is true for the class above, and what instrument you would use instead.

Exercise 4 — the loud direction. struct B { B(B&&) noexcept {} }; — say what B b = a; does and quote the diagnostic.

Exercise 5 — the cost. For a class with a destructor, deep copies and no moves, estimate the allocations for eight push_backs with reserve(1), using Chapter 13.2 §6's formula.

Exercise 6 — two shapes. Write the five declarations for a value-like buffer and for a move-only file handle.

Exercise 7 — why suppress? Explain why the compiler declining to generate a move for a class with a destructor is the right default.

Exercise 8 — zero. Rewrite a raw-pointer buffer class so that all five are correctly generated, and say what triggers nothing.

15. Summary

  • Five functions, and declaring one changes which others exist — asymmetrically.
  • A destructor (or either copy) suppresses both moves; either move deletes both copies.
  • Measured, loud: declaring a move gives implicitly-deleted copy constructor … user-declared move constructor.
  • Measured, silent, by counting: HasDtor ran a copy, not a move — no move constructor exists.
  • The trait probe said otherwise because is_move_constructible is satisfied by a copy constructor; its zeros were reliable, its ones were not.
  • Method lesson: count which function runs — the fourth time in this curriculum a broader probe gave a confident wrong answer.
  • The suppression is a defensible default — a generated move would double-own; silently slow beats silently wrong.
  • Measured cost: 7 copies / 11 allocations versus 0 / 7 moves / 4.
  • Declared a destructor? Declare the moves. Two coherent shapes: all five, or move-only.
  • = default where the compiler's version is right, not an empty body.
  • The tell: a hand-written destructor and no moves means silent copying.
  • The right number is usually zero.

Every chapter in this module has now ended by pointing at the same place, and the arithmetic has become hard to ignore: a class that directly owns a raw resource needs five carefully written functions, each with its own failure mode measured somewhere in these pages — a double free, a leak, a use-after-free on self-assignment, a silently suppressed move, a container quietly copying because a keyword was omitted. A class that holds a member which already does all of that needs none of them, and gets all five generated correctly. The next chapter is the argument that the second kind of class is not a special case but the normal one, that the five-function discipline is a thing you should know how to do and almost never do, and that the measured difference between the two designs is twelve lines of resource management against zero.

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.