Module 10 — Copying and Resource Management¶
After this module you can predict every copy and move your program performs, you can explain the Rule of Three, the Rule of Five and the Rule of Zero from experience, and you will default to designs that need none of them.
At a glance¶
| Prerequisites | Module 9 |
| Estimated study time | 10–12 hours (the most demanding module in the course) |
| Lab | Lab 10 — reproduce the shallow-copy bug, then Rule of Five, then Rule of Zero |
| Exercises | Three tiers — warm-up, core, challenge |
| Cognitive levels | L4 Analyze → L7 Refactor — exit level L7 Refactor |
| Self-assessment | Module quiz |
Why this module matters¶
This is the module that separates programmers who use C++ from programmers who understand it.
The problem it solves is small to state and severe in consequence. When an object
is copied, C++ copies its members one by one. For a member that holds a value —
a number, a std::string — that is exactly right. But when a class manages a
resource and holds a raw pointer to it, the member-wise copy duplicates the
address, and now two objects believe they own the same thing. Both will free it.
The program has a bug that may not appear for hours, and will not appear at all in
some builds.
You will create this bug deliberately in the lab, look at the damage, and fix it properly — and then you will make the fix unnecessary, which is the real lesson. The best resource-managing class in modern C++ is one that owns no raw resource directly, so every special member function is correct without being written.
Along the way you learn move semantics, which is not an optimisation curiosity but the mechanism that makes returning containers cheap and the Rule of Zero viable.
This module contains deliberately broken code
The example programs under examples/module-10/ include undefined behaviour
on purpose: double-free and use-after-move. Each such file states this in its
header, and explains what the reader should look for. Do not copy those
patterns; the point is to recognise them.
Learning objectives¶
By the end of this module you will be able to:
- Explain what the compiler-generated copy constructor and copy assignment operator do, and when a member-wise copy is wrong.
- Distinguish shallow copying from deep copying, and demonstrate the failure a shallow copy causes for an owning class.
- Write special member functions correctly:
- the copy constructor,
- the copy assignment operator (self-assignment safe),
- the destructor,
- the move constructor and move assignment operator.
- State the Rule of Three, the Rule of Five and the Rule of Zero, and apply the right one to a given class.
- Explain what the compiler generates, and what declaring one special member function does to the others.
- Explain lvalues and rvalues well enough to say why
T&&binds where it does. - Explain that
std::moveis a cast, not a move, and that a moved-from object is valid but unspecified. - Explain why move operations should be
noexcept, and what breaks instd::vectorwhen they are not. - Use
= deleteto make a type non-copyable, and say when that is right. - Explain copy elision, including the C++17 guarantee in initialisation contexts, and explain why it means "not copied" but not "not copyable".
- Instrument a class with copy and move counters, predict the counts for a given expression, and verify — the single most effective exercise in the module.
Core concepts¶
- Copy constructor — constructs a new object as a copy of an existing one:
T(const T&). - Copy assignment — replaces an existing object's contents:
T& operator=(const T&). - Move constructor — constructs a new object by taking over an existing
object's resources:
T(T&&), typically leaving the source empty but valid. - Move assignment —
T& operator=(T&&). - Destructor — releases what the object owns.
- Special member functions — the five above. The compiler generates versions of those it can, and its choices depend on which ones you declare yourself.
- Rule of Three — if you need a destructor, a copy constructor or a copy assignment operator, you almost certainly need all three.
- Rule of Five — the Rule of Three plus the move constructor and move assignment, in modern C++.
- Rule of Zero — the goal: a class that manages no resource directly, relies
on members that do (
std::string,std::vector, smart pointers), and therefore needs to declare none of the five. - Shallow copy — copying the pointer, not what it points at.
- Deep copy — allocating and copying the pointed-to resource, so the two objects are independent.
- Double free — both owners delete the same resource. Undefined behaviour, and usually a crash under any real workload.
- Self-assignment —
a = a. A copy assignment that frees before allocating destroys its own source; it must handle this case. - Copy-and-swap — a technique that makes assignment correct and self-assignment safe by copying first, then swapping. Costs one extra copy; buys clear reasoning.
- lvalue / rvalue — loosely, an expression with a name that persists (lvalue) versus a temporary or a value being moved from (rvalue).
- Rvalue reference —
T&&; binds to rvalues, and is what makes move operations selectable. std::move— an unconditional cast to an rvalue reference. It moves nothing by itself; it merely marks an expression as safe to plunder.- Moved-from state — valid but unspecified. Never assume a moved-from object is empty; do assume it can be destroyed and reassigned.
noexceptmoves — astd::vectorwill not use a move constructor during reallocation unless it isnoexcept, because moving the whole buffer must not fail halfway.= delete— explicitly removes a member function; a non-copyable type is made by deleting the copy constructor and copy assignment.- Copy elision — the compiler constructing an object directly where it is needed, so no copy occurs. In C++17 elision is guaranteed in initialisation from a prvalue.
explicit— also relevant here: it blocks the implicit conversions that can silently create copies.
Module contents¶
| Section | What it contains |
|---|---|
| Introduction (this page) | Overview, prerequisites, objectives, concept inventory |
| Concepts and explanations | The shallow copy and the double free; why the compiler's copy exists; the Rule of Three, self-assignment and copy-and-swap; moves, std::move, the moved-from state and self-move; noexcept and container guarantees; the Rule of Five and the Rule of Zero; what the compiler still does |
| Worked examples | Six programs — the copy that shares one block, the Rule of Three counted, copy-and-swap and the strong guarantee, moves measured one expression at a time, seven moves becoming seven copies, and the Rule of Zero with elision |
| Code examples | examples/module-10/ — six programs, each with captured output, two of which contain deliberate undefined behaviour and say so in their header |
| Misconceptions and mistakes | Nine wrong mental models with the program that contradicts each, ten symptoms, and six worked mistakes with their real compiler and linker messages |
| Debugging | Counting, and letting a sanitizer name both stacks; the double-free report read line by line; what to do when the sanitizer is missing from your toolchain |
| Exercises | Three tiers, from naming which functions the compiler generates to refactoring a working class without changing its behaviour |
| Lab 10 | The catalogue's hand-managed array: reproduce the double free, fix it with the Rule of Five, then delete four of those five functions |
| Challenge problems | A bespoke fixed-size buffer to refactor, a movable-only resource handle to build, and two implementations to judge |
| Self-assessment | Ten questions, four of them predictions, including one that measures the module's central claim |
| Summary | What must be retained, the terms to be able to define, and the checklist |
| Further reading | Books and official documentation |
What you can do after this module¶
- Point at any class and say whether its compiler-generated copies are correct.
- Take an owning class, reproduce the double-free, and fix it with the Rule of Five.
- Then delete four of those five functions by redesigning to the Rule of Zero, and explain what made that possible.
- Predict copy and move counts from a piece of source, and verify with counters.
- Explain why
std::moveon aconstobject silently produces a copy.
Where this is used later¶
- RAII and smart pointers are the Rule of Zero made concrete: Module 11.
noexceptmoves matter again when containers panic-free operations are needed in Module 13.- Types used as container elements — including what a container requires of them — are covered in Module 14.
- Copy elision and moves affect how the capstone passes objects between components in Module 16.
Common mistakes to watch for¶
| Symptom | Cause |
|---|---|
Crash on destruction, or double free detected |
Shallow copy of an owning pointer: two objects delete the same resource |
| Corrupted data after a copy; changes to one object appear in the other | Shallow copy where a deep copy was needed |
Crash only when a = a |
Copy assignment destroys its own source before reading it |
warning: 'x' is used after being moved style diagnostics, or nonsense after std::move |
A moved-from object was used as though it still held its value |
| A copy happens where a move was expected | The object was const, or the move operations are not declared, or they are not noexcept in a reallocating container |
error: use of deleted function copying an object |
The class is deliberately non-copyable — pass by reference, or return a smart pointer |
| Code that "works" only in a debug build | Undefined behaviour. Freeing twice, or reading freed memory, is not required to fail |
| A class with a destructor but no copy constructor | Exactly the Rule of Three violation. The compiler generated a shallow copy |
Skills checklist¶
Levels this module works at: [L4 Analyze → L7 Refactor] — see How you will learn.
Tick an item only when the evidence exists:
- [ ] I can explain why the compiler's copy of an owning class double-frees — evidence: the two destructor lines freeing one address, and your explanation of why the crash happens at destruction rather than at the copy
- [ ] I can write a correct copy constructor and copy assignment for an owning class —
evidence: counters showing one allocation per object and
blocks alive 0 - [ ] I can make assignment self-assignment safe — evidence:
a = arun before and after your fix, including what went wrong before - [ ] I can write move operations and prove they are used — evidence: the before/after counter trace for the same scripted sequence
- [ ] I can explain what
std::movedoes and what a moved-from object is for — evidence: the expression wherestd::moveproduced a copy anyway, and why - [ ] I can say why
noexceptmatters on a move — evidence: the reallocation counts with and without it, and the one-linestatic_assertthat would have caught it - [ ] I can refactor an owning class to the Rule of Zero with unchanged observable behaviour — evidence: Lab 10's three stages, with the identical output from the last two
- [ ] I can judge two implementations of the same class — evidence: challenge 3's written evaluation, naming a real cost of the option you chose
Suggested next step¶
Previous: Module 9 — Composition and Association · Next: Module 11 — RAII and Smart Pointers · All modules: Modules index
Module 11 makes this module's answer into library types. You have just written five functions to make one owning class safe; Module 11 replaces the resource with a member whose type does the owning, and the five functions stop being your problem.