Module 11 — RAII and Smart Pointers¶
After this module you can write a class that cleans up correctly no matter how it exits, and you can replace raw owning pointers with ownership types that the compiler checks for you.
At a glance¶
| Prerequisites | Module 10 |
| Estimated study time | 8–10 hours |
| Lab | Lab 11 — convert the running system's ownership to smart pointers, with evidence |
| 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¶
Module 10 ended with the Rule of Zero: the best resource-managing class is one that owns no resource directly. This module supplies the members that make that possible — and names the idea behind all of it, which turns out to be the most important design idea in C++.
RAII — Resource Acquisition Is Initialisation — is the observation that
every resource should be owned by an object, acquired in its constructor and
released in its destructor. Because C++ guarantees destructors run when an object
goes out of scope, including during an exception, this single rule makes cleanup
automatic rather than remembered. It is the reason this language does not need
finally, and the reason a Python programmer can read C++ and find no resource
management anywhere.
Once you accept that, the question changes from "where do I call delete?" to "who
owns this, and is that ownership expressed in the type?". Smart pointers are how
C++ answers it:
std::unique_ptr— exclusive ownership. Cannot be copied, only moved, so the compiler enforces that there is exactly one owner.std::shared_ptr— shared ownership, by reference count. Deliberate, and slightly expensive. Not the default.std::weak_ptr— a non-owning observer of shared state, which also solves the problem shared ownership creates: cycles that never free.
By the end of the module, new and delete should appear in your own code
approximately never, and that should feel like a relief rather than a restriction.
Learning objectives¶
By the end of this module you will be able to:
- State what RAII means, and explain why the destructor is the only reliable cleanup point.
- Write an RAII class that acquires in its constructor and releases in its destructor, and prove with output that it cleans up on both the normal and the exceptional exit path.
- Explain why a destructor must not throw, and what
noexceptimplies for it. - Use
std::unique_ptras the default way to express exclusive ownership, and explain why it cannot be copied. - Transfer ownership with
std::moveand a by-value or&¶meter, and explain what the function signature communicates to a reader. - Use
std::shared_ptrwhere ownership is genuinely shared, and state its costs (reference counting, a separate control block, and the possibility of cycles). - Use
std::weak_ptrto observe shared state without extending its lifetime, and to break ashared_ptrcycle. - Prefer
std::make_uniqueandstd::make_sharedto rawnew, and explain what the factory functions do better. - Hold polymorphic objects in
std::vector<std::unique_ptr<Base>>— ownership and polymorphism together. - Explain why a raw pointer parameter usually means "may be null, does not own", and why a raw owning pointer is a design smell.
- Rewrite a class to the Rule of Zero by delegating ownership to a smart pointer.
- Use a custom deleter for a resource that is not memory (for example a file handle), and say when that is worth the complexity.
Core concepts¶
- RAII — every resource is owned by an object: acquired in a constructor, released in a destructor.
- Automatic cleanup — destructors run when a scope ends, including when the scope ends because of an exception. This is why RAII is exception-safe by construction.
- Stack unwinding — the process of destroying local objects while an exception propagates. See Module 13.
- Resource — anything that must be released: heap memory, a file, a socket, a lock, a device handle.
noexceptdestructor — destructors must not throw. A throw while an exception is already propagating terminates the program.- Ownership — the responsibility for a resource's lifetime. Express it in the type.
std::unique_ptr<T>— exclusive ownership, zero overhead compared with a raw pointer, movable but not copyable, and it deletes the object when it dies.std::make_unique<T>(args...)— the way to create one (C++14). Avoids writingnew, and cannot leak if a later argument throws.std::shared_ptr<T>— shared ownership through a reference count. The object is destroyed when the lastshared_ptrto it goes away.std::make_shared<T>(args...)— allocates the object and its control block together, which is usually more efficient than two allocations.- Control block — the bookkeeping structure behind a
shared_ptr(use count, weak count, deleter). Not part of the object. std::weak_ptr<T>— observes an object owned byshared_ptrwithout affecting its lifetime;lock()produces ashared_ptrif the object still exists.- Reference cycle — two
shared_ptrs owning each other. Neither count reaches zero, so neither is destroyed.weak_ptrbreaks the cycle. - Moving ownership —
std::move(p); the source becomes null. This is how ownership is passed, and the signature should show it. - Custom deleter — a callable given to a smart pointer so it releases a non-memory resource correctly.
- Raw owning pointer — a raw pointer whose target you must
delete. A smell: use a smart pointer so the ownership is visible in the type. - Rule of Zero, achieved — a class whose members are
std::string,std::vectorand smart pointers needs none of the five special member functions, and is correct anyway.
Module contents¶
| Section | What it contains |
|---|---|
| Introduction (this page) | Overview, prerequisites, objectives, concept inventory |
| Concepts and explanations | RAII as a property of control flow; a wrapper for a non-memory resource; what a raw pointer does not say; unique_ptr, borrowing signatures, shared_ptr and its control block, weak_ptr and cycles, enable_shared_from_this, polymorphic containers, and the conversion table |
| Worked examples | Six programs — three exit paths with and without RAII, three memory bugs made countable, what unique_ptr costs and forbids, the cycle no tool reports, two control blocks for one object, and resources that are not memory |
| Code examples | examples/module-11/ — five programs, each with captured output |
| Misconceptions and mistakes | Nine wrong mental models with the program that contradicts each, ten symptoms, and five worked mistakes with their real compiler messages |
| Debugging | Instrumenting acquisition and release; the instance counter that finds legal leaks; the tools that miss them, and a diagnosis that began with "created but never destroyed" |
| Exercises | Three tiers, from eight declarations to read to a refactor proved by counters on the throwing path |
| Lab 11 | Every resource owned by exactly one thing: the ownership diagram compiled, with proof on both the normal and the throwing run |
| Challenge problems | Ownership for a cache with evictions, accounting for every pointer in a raw-pointer system, and two designs to judge |
| Self-assessment | Nine questions, including a program whose correct output is nothing at all |
| 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¶
- Write a class that releases its resource whether it returns normally or throws, and demonstrate both paths.
- Choose between
unique_ptr,shared_ptrand a value member on ownership grounds. - Explain why
unique_ptrbeing non-copyable is a feature, not an inconvenience. - Break a reference cycle that leaks, and prove that it no longer leaks.
- Convert an existing class to the Rule of Zero and count how much code disappeared.
Where this is used later¶
- Unwinding and cleanup on the exception path are examined properly in Module 13, where exception safety is defined.
- Containers of smart pointers are used with the standard algorithms in Module 14.
- Factory functions that return
unique_ptrbecome the factory pattern in Module 15. - Ownership across the whole application is the architecture question of the capstone, Module 16.
Common mistakes to watch for¶
| Symptom | Cause |
|---|---|
error: use of deleted function 'std::unique_ptr<...>::unique_ptr(const ...)' |
Copying a unique_ptr. Ownership is exclusive: move it instead |
A null-pointer dereference after std::move(p) |
The moved-from unique_ptr is null. Do not keep using it |
| A leak that valgrind or a sanitizer reports as "still reachable" | A shared_ptr cycle. Look for two objects that own each other, and make one link a weak_ptr |
error: no matching function for call to 'std::make_unique<Shape>(...)' |
make_unique needs a concrete type; an abstract base cannot be created |
| The program terminates when a destructor runs | A destructor threw during unwinding. Destructors must not throw |
shared_ptr slows a hot path unexpectedly |
Reference counting is not free. Where ownership is exclusive, use unique_ptr |
Two shared_ptrs created from the same raw pointer |
Two control blocks, two deletions. Use make_shared, or take a shared_ptr by value/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 RAII and apply it to a non-memory resource — evidence: my own wrapper, releasing on the normal path, the early-return path and the throwing path
- [ ] I can say what each smart pointer owns — evidence: the mapping from every ownership-diagram label in Lab 11 to a declaration, with the type named
- [ ] I can convert raw owning pointers to
unique_ptrand fix every error the change produces — evidence: the conversion, and each error explained in one line - [ ] I can reproduce and fix a reference cycle — evidence: the silent destructors, the
weak_ptrfix, and the destructor lines that then appear in an order I predicted - [ ] I can diagnose a second control block — evidence: exercise C3's two fixes, with one preferred for a stated reason
- [ ] I can choose between owning and borrowing in a signature — evidence: one function changed
from an owning parameter to a reference, with the reason, and the
std::movecall that disappeared with it - [ ] I can prove cleanup happens on every path — evidence: two counter runs, one of which throws and is caught at a point where the counts can still be printed
- [ ] I can say where I rejected
shared_ptr, and why — evidence: the written note from Lab 11
Suggested next step¶
Previous: Module 10 — Copying and Resource Management · Next: Module 12 — Templates · All modules: Modules index
Module 12 multiplies every decision on this page: generic code has to be correct not for one type
but for several, which means it has to be correct for whatever ownership those types use. Lab 12
asks for one Repository<T> serving two record kinds, and its first question is whether your types
can be copied at all — which Modules 10 and 11 have just settled.