Skip to content

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:

  1. State what RAII means, and explain why the destructor is the only reliable cleanup point.
  2. 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.
  3. Explain why a destructor must not throw, and what noexcept implies for it.
  4. Use std::unique_ptr as the default way to express exclusive ownership, and explain why it cannot be copied.
  5. Transfer ownership with std::move and a by-value or && parameter, and explain what the function signature communicates to a reader.
  6. Use std::shared_ptr where ownership is genuinely shared, and state its costs (reference counting, a separate control block, and the possibility of cycles).
  7. Use std::weak_ptr to observe shared state without extending its lifetime, and to break a shared_ptr cycle.
  8. Prefer std::make_unique and std::make_shared to raw new, and explain what the factory functions do better.
  9. Hold polymorphic objects in std::vector<std::unique_ptr<Base>> — ownership and polymorphism together.
  10. Explain why a raw pointer parameter usually means "may be null, does not own", and why a raw owning pointer is a design smell.
  11. Rewrite a class to the Rule of Zero by delegating ownership to a smart pointer.
  12. 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.
  • noexcept destructor — 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 writing new, and cannot leak if a later argument throws.
  • std::shared_ptr<T> — shared ownership through a reference count. The object is destroyed when the last shared_ptr to 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 by shared_ptr without affecting its lifetime; lock() produces a shared_ptr if the object still exists.
  • Reference cycle — two shared_ptrs owning each other. Neither count reaches zero, so neither is destroyed. weak_ptr breaks 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::vector and 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_ptr and a value member on ownership grounds.
  • Explain why unique_ptr being 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_ptr become 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_ptr and 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_ptr fix, 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::move call 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.