Skip to content

Practice Lab 21 — Smart pointer object manager

Difficulty: Advanced · Estimated time: 6 hours · Module 11: RAII, Dynamic Memory and Smart Pointers

A resource ownership simulation. Every object in the system owns something, every exit path must release it, and the counters are the proof that it does.


At a glance

Difficulty Advanced
Estimated time 6 hours
Module Module 11 — RAII, Dynamic Memory and Smart Pointers
Concepts practised Stack versus heap, new/delete, ownership, unique_ptr, shared_ptr, weak_ptr, RAII, cleanup on every exit path, ownership design
Prerequisite Practice Lab 20
Class design yours — the requirement is that no raw new survives in the final version

Scenario

A document server keeps documents open for sessions. Each session owns a document handle (a real resource: a buffer with a destructor), and several sessions may share one document. Handles are opened and closed by different code paths, and one path exits through an early return while another exits through a thrown exception.

The server has leaked for two releases: the resource count at exit is never zero, and nobody can work out which path leaks because the leak is small and the server restarts nightly.

This lab builds the same server twice: once with raw ownership, deliberately leaking on one path, and once with RAII and smart pointers, where the same path releases everything.


Problem statement

Implement:

  1. A resource type that owns something real — a dynamically allocated buffer is enough — and narrates every acquisition and release.
  2. A raw-ownership version of the session manager: owning pointers, manual cleanup, and one exit path that leaks, kept deliberately so the failure is visible.
  3. An RAII version of the same manager with:
  4. std::unique_ptr for ownership;
  5. std::shared_ptr where a document is genuinely shared;
  6. std::weak_ptr for observers that must not keep a document alive;
  7. no bare new or delete anywhere — prove it by searching.
  8. An RAII guard: a small type that acquires in its constructor and releases in its destructor, used to wrap one non-memory resource (a "lock" counter is enough, as long as it is real in your program).
  9. Three exit paths: normal completion, an early return, and an exception — in both versions, with the resource count printed after each.

In ownership.md:

  • The leak in the raw version: which path, and the count that proves it.
  • The same path in the RAII version: the count that proves it does not leak.
  • For each owned object in your system: the smart pointer that expresses its ownership and one sentence saying why that one.
  • The case where you used a weak_ptr, and what would have happened with a shared_ptr instead (including whether the count at exit would have been zero).

Learning objectives

By the end of this lab you will be able to:

  • Reproduce a leak on a specific exit path and explain it as "release happens after the exit".
  • Convert ownership to smart pointers and explain what each pointer type promises.
  • Choose unique_ptr, shared_ptr or weak_ptr from the ownership relationship rather than from habit.
  • Write an RAII guard for a non-memory resource and show it releases on the exception path.
  • Prove, with counts, that no path leaks — and identify the case where a smart pointer is the wrong answer.

Functional requirements

  1. The raw version leaks on exactly one documented path, and the counts show it.
  2. The RAII version leaks on no path, and the counts prove it after all three paths.
  3. Documents shared by several sessions are destroyed after the last session releases them — the release count is one.
  4. An observer holding a weak_ptr does not keep a document alive — demonstrated by the destruction happening while the observer still exists.
  5. An observer that tries to use an expired document gets a documented result, not undefined behaviour.
  6. No bare new/delete in the final version: searched for, with the search recorded.
  7. Ownership is expressed in the types: a manager owns unique_ptrs, sessions hold a shared_ptr when sharing and a reference when merely using.
  8. The RAII guard releases exactly once, including when an exception unwinds past it.
  9. Destruction order is documented and matches the output.
  10. The final count after main ends is zero for every resource kind, printed from a destructor-owned counter.

Constraints

  • Header and source per type family, with a documented build command.
  • The raw version must exist in the repository — in a separate small program or clearly separated in the same file — because reproducing the leak is part of the lab. Label it.
  • std::make_unique and std::make_shared only; no new in the final version.
  • No shared_ptr where a unique_ptr would do; each use of sharing must be justified in the notes.
  • No warnings, including no unused-variable warnings for the deliberately leaked raw version.

Starter task

In ownership.md, before coding:

  1. List the resources: what is owned, by whom, and for how long. Include the one non-memory resource.
  2. For each, choose the pointer type and write the sentence that forces the choice.
  3. Predict the counts after each of the three exit paths, in both versions. Six predictions.
  4. Write the leak you expect in the raw version, with the line that causes it.

Implementation tasks

  1. Implement the resource with narration and a static live count.
  2. Implement the raw version with the open, the early return, the throwing path, and the leak.
  3. Run all three paths and record the counts.
  4. Implement the RAII version, replacing each raw pointer by choice, and adding the guard.
  5. Run the same three paths and record the counts; compare with the predictions.
  6. Add the observer with a weak_ptr, let the document expire, and record what the observer can and cannot do.
  7. Search for new and delete in the final version and paste the result into the notes.

Test cases

# Action Expected result
1 Raw version, normal path Counts return to zero
2 Raw version, early return A non-zero count, and the leaking line identified
3 Raw version, exception path The count recorded, with the reason
4 RAII version, normal path Zero at exit
5 RAII version, early return Zero at exit
6 RAII version, exception path Zero at exit
7 Two sessions sharing one document Release count 1, after the second session
8 Observer with an expired document Documented result, no undefined behaviour, destruction already narrating
9 The RAII guard with an exception thrown inside its scope Released exactly once
10 Search for new/delete in the final version Nothing found — output pasted
11 Destruction order across the three types Matches the documented order
12 Final counts after main Zero for buffers, documents and locks

Boundary cases to run deliberately: a session with no document; a document shared by three sessions in different scopes; an exception thrown before the guard is constructed.


Expected behaviour

raw version, early return:
  [buffer] acquired (live 1)
  early return taken
  live buffers at exit: 1        <- the leak
RAII version, same path:
  [buffer] acquired (live 1)
  early return taken
  [buffer] released (live 0)
  live buffers at exit: 0
shared document, two sessions:
  [document] created (shared count 1)
  session A holds it      (shared count 2)
  session B holds it      (shared count 3)
  session A released      (shared count 2)
  session B released      (shared count 1)
  [document] destroyed, release count 1
observer with weak_ptr: document destroyed while observer exists; use() -> expired
grep for "new " and "delete " in the RAII version: no matches
final live counts: buffers 0, documents 0, locks 0

Extension challenge

Make the manager handle a failing acquisition: the resource's constructor throws for one specific identifier.

Record in ownership.md:

  • What happens to the other resources in flight when the failure occurs, with the counts.
  • Whether the strong guarantee holds, and what would break it.
  • Which smart pointer made the cleanup automatic, and what the raw version would have had to write by hand.
  • The change cost: files, operations, lines.

Debugging challenge

The RAII version still leaks on one path, and there is no new in sight:

void handle_session(const std::string& id) {
    Resource* raw = acquire_raw(id);         // a C-style factory, still used "temporarily"
    auto buffer = std::unique_ptr<Resource>(raw);
    if (!is_valid(id)) {
        release_raw(raw);                    // "release it the old way"
        return;                              // ...and then the unique_ptr releases it AGAIN
    }
    use(buffer);
}

Reproduce it — have release_raw narrate and count — and answer:

  1. Which two owners released the same resource, and what does the count show?
  2. Why is the result undefined rather than merely wrong?
  3. Which line must go, and what rule does its presence break?
  4. What would have made the mistake impossible: wrapping the C-style factory in its own RAII type, controlling the factory's interface, or something else? Give the cost of your answer.

Reflection questions

  1. Your leak was on a specific path. What is the general rule that prevents that class of leak?
  2. Which ownership is unique_ptr, which is shared_ptr, and which is weak_ptr? Say why for each, in the form "X owns Y because …".
  3. The observer used a weak_ptr. What would a shared_ptr have done to the destruction count, and why is that a bug rather than a leak?
  4. Sharing has a cost: a control block, an atomic count, and a lifetime that is now decided at run time. Where in your design is that cost justified, and where did you avoid it?
  5. Your guard wraps a non-memory resource. What is the general shape of an RAII type, and where else in this course could it have been used?
  6. Which raw pointer remains in your design (a non-owning one, perhaps), and how does a reader know it does not own? Answer with what you wrote in the code.

Completion checklist

  • [ ] It builds from a documented command with -Wall -Wextra -Werror and no warnings
  • [ ] The raw version leaks on a named path, and the count proves it
  • [ ] The RAII version returns to zero on all three paths
  • [ ] No bare new/delete in the final version — search recorded
  • [ ] Every sharing use is justified; no shared_ptr where unique_ptr would do
  • [ ] The weak_ptr observer does not prolong a document's life — demonstrated
  • [ ] The RAII guard releases exactly once, including on the exception path
  • [ ] Ownership is expressed in the types, with the choice sentence written for each
  • [ ] All twelve test cases run, with output recorded
  • [ ] The debugging challenge was explained before being fixed
  • [ ] The final live counts are zero, printed from a counter, not asserted
  • [ ] ownership.md states the pointer choice for every owned object in the system

Related: Lab index · Module 11 — RAII and Smart Pointers · Previous: Practice Lab 20 · Next: Practice Lab 22 — Device session guard with custom deleters