This page is the practice arena for the parent topic . We will not re-explain the theory — instead we take the full space of situations smart pointers can land you in and work each one to the end , watching the reference count tick like a stopwatch.
Before any code, one word you must own: the reference count (also called the strong count ). It is just an integer stored next to the object that says "how many owners are alive right now". Each new owner adds 1 ; each dying owner subtracts 1 ; at 0 the object is deleted. That single counter is the heartbeat of everything below. See Reference-counting if the idea is new.
Every problem in this topic is one of these cells . The examples afterwards each announce which cell they fill, and together they cover the whole grid.
Cell
What varies
The tricky part
A. unique — happy path
one owner, scope exit
destructor fires exactly once
B. unique — move transfer
ownership hops object→object
source becomes nullptr
C. unique — the "zero" / empty case
pointer holds nothing
dereferencing a nullptr
D. shared — count goes up then down
copies enter/leave scopes
count returns to where it started
E. shared — the double control-block trap
two shared_ptr from one raw
count splits , double-free
F. weak — object still alive
lock() succeeds
count momentarily + 1
G. weak — object already gone (expired)
last owner died first
lock() returns empty
H. the cycle (degenerate/leak)
A↔B both strong
count never reaches 0
I. real-world word problem
observer cache
mix of shared + weak
J. exam twist
count arithmetic under nesting
predict the exact integer
Two of these are "limiting / degenerate" cases you must never skip: C (the empty pointer — the zero input ) and H (the cycle — the count that limits to 1 instead of 0 ). We give both their own figure.
Worked example One owner, one automatic delete
#include <memory>
#include <iostream>
struct Loud {
Loud () { std ::cout << "born \n " ; }
~Loud () { std ::cout << "died \n " ; }
};
void f () {
auto p = std :: make_unique < Loud >(); // (1)
std ::cout << "working \n " ; // (2)
} // (3)
Forecast: How many times does ~Loud (the destructor) run, and where? Guess before reading.
Step 1 — construct. make_unique<Loud>() does the new, prints born, and hands ownership to p.
Why this step? We want the object's lifetime bolted to p's scope — no raw new we could forget.
Step 2 — use. Prints working. The object is alive; count of owners is exactly 1 .
Why this step? To show nothing special happens during normal use — a unique_ptr dereferences like a raw pointer.
Step 3 — scope exit. At the closing brace p's destructor runs, which does delete, which prints died.
Why this step? This is RAII in one line: the resource dies with the manager. No delete was written by hand.
Output: born / working / died — destructor runs exactly once .
Verify: born-count = 1 , died-count = 1 , and they are equal → no leak, no double-free. Balanced.
Worked example Ownership hops; the source is nulled
auto p = std :: make_unique < int >( 42 ); // (1) p owns 42
auto q = std :: move (p); // (2) transfer
bool p_empty = (p == nullptr ); // (3)
bool q_has = (q != nullptr ); // (4)
int value = * q; // (5)
Forecast: After the move, is p empty or does it still point at 42? What is *q?
Step 1 — construct. p is the sole owner of an int holding 42 .
Step 2 — move. std::move casts p to an rvalue so the move constructor runs (copy is = deleted — see Move-semantics ). It steals the inner pointer: q now holds it, p is set to nullptr.
Why this step? Copying two unique_ptrs to one object would double-delete. Moving keeps the invariant "exactly one owner" — count stays 1 , just relocated.
Step 3–4 — check. p == nullptr is true; q != nullptr is true.
Why this step? To prove ownership moved , it wasn't duplicated .
Step 5 — read. *q == 42.
Verify: p_empty == true, q_has == true, value == 42. Exactly one owner throughout — invariant preserved.
Worked example Dereferencing nothing
std ::unique_ptr <int> p; // (1) default = holds nullptr
bool empty = ! p; // (2)
// std::cout << *p; // (3) ⚠️ undefined behaviour!
if (p) std ::cout << * p; // (4) guarded — prints nothing
Forecast: What does a default-constructed unique_ptr hold, and what happens if you dereference it?
Step 1 — construct empty. With no argument, p holds nullptr — it owns nothing . This is the zero input of pointer land: valid to have , illegal to use .
Why this step? Every scenario table must cover the degenerate case. An owner of nothing is legal; a use of nothing is not.
Step 2 — test. unique_ptr converts to bool: !p is true when empty. So empty == true.
Why this step? This is the safe way to ask "do I actually own something?"
Step 3 — the trap. *p on a null pointer is undefined behaviour — usually a crash. Look at the figure: the arrow points at the void.
Step 4 — the fix. Guard with if (p). Because p is empty, the body is skipped; nothing prints, nothing crashes.
Verify: empty == true; the guarded branch is never entered, so program output is empty. No dereference occurs.
Worked example The count returns to where it started
auto a = std :: make_shared < int >( 7 ); // (1) strong = 1
long c1 = a. use_count (); // (2)
long c2;
{
auto b = a; // (3) copy → strong = 2
c2 = a. use_count ();
} // (4) b dies → strong = 1
long c3 = a. use_count (); // (5)
Forecast: Write down the three integers c1, c2, c3 before you read on.
Step 1 — make. One allocation for both the int and its control block; strong count = 1 .
Why make_shared? One allocation instead of two → faster, cache-friendlier.
Step 2 — read. c1 = 1.
Step 3 — copy. b = a copies the shared_ptr, which does strong++. Now c2 = 2. Both a and b point at the same control block — this is the crucial difference from Example 5.
Why this step? Copying a shared_ptr is the legal way to add an owner (unlike a unique_ptr, which forbids it).
Step 4 — inner scope exits. b's destructor does strong--. Back to 1 .
Step 5 — read. c3 = 1. When a finally dies, strong hits 0 and the int is deleted.
Verify: c1 == 1, c2 == 2, c3 == 1. Count came back to its starting value → symmetric up/down → correct.
shared_ptrs from one raw pointer
int* raw = new int ( 7 );
std :: shared_ptr < int > a ( raw ); // (1) control block #1, strong = 1
std :: shared_ptr < int > b ( raw ); // (2) control block #2, strong = 1 ⚠️
long ca = a. use_count (); // (3)
long cb = b. use_count (); // (4)
// both destructors delete the SAME int → double free (crash)
Forecast: What does a.use_count() report — 2 (they share) or 1 (they don't)?
Step 1 — first wrap. a builds a fresh control block around raw; its strong count is 1 .
Step 2 — second wrap. b(raw) has no idea a exists. It builds a second, independent control block, also starting at 1 . Now two separate counters each believe they are the sole owner of the same int.
Why this step? Reference counting only coordinates through the control block . Sharing a raw address does not share the counter.
Step 3–4 — read. ca == 1 and cb == 1 — proof that they did not merge. When each dies it deletes the same address → double-free.
The fix. Make one shared_ptr and copy it (Example 4), or use make_shared.
Verify: ca == 1 and cb == 1 (not 2 ). Independent counts confirm the bug. See Memory-leaks-and-dangling-pointers .
Worked example Observing, then briefly pinning
auto s = std :: make_shared < int >( 99 ); // (1) strong = 1
std ::weak_ptr <int> w = s; // (2) strong still 1
long during;
int got;
if ( auto locked = w. lock ()) { // (3) strong → 2 inside
during = s. use_count ();
got = * locked;
} // (4) locked dies → strong 1
long after = s. use_count ();
Forecast: Does building w change the strong count? What is the count inside the if?
Step 1 — own. s owns the int(99); strong = 1 .
Step 2 — observe. A weak_ptr records the weak count, not the strong count. So making w leaves strong at 1 — it does not keep the object alive.
Why this step? This is the whole point of weak_ptr: it looks without owning.
Step 3 — lock. w.lock() atomically checks "is the object still there?" Yes → it returns a real shared_ptr (locked), which bumps strong to 2 for the duration. So during == 2, and got == 99.
Why lock() and not *w? A weak_ptr has no *. Between "check alive" and "use" another thread could delete it; lock() fuses those two steps so you can never touch a dangling object.
Step 4 — release. locked leaves scope, strong drops back to 1 . after == 1.
Verify: during == 2, got == 99, after == 1. The weak observation added nothing; only the temporary lock moved the count.
Worked example Locking after the last owner died
std ::weak_ptr <int> w;
{
auto s = std :: make_shared < int >( 5 ); // (1) strong = 1
w = s; // (2) observe
} // (3) s dies → strong 0 → int deleted
bool expired = w. expired (); // (4)
auto locked = w. lock (); // (5) returns empty
bool got_null = (locked == nullptr ); // (6)
Forecast: After the block ends, is the object still there? What does lock() hand back?
Step 1–2 — own and observe. s owns int(5); w watches without counting.
Step 3 — the owner leaves. At the brace, s dies, strong → 0 , the int is deleted . The control block survives only because w still holds a weak reference to it (so expired() can answer honestly).
Why this step? This is the limiting case of Cell F: the strong count crossed 0 before we tried to lock.
Step 4 — ask. w.expired() is true because strong = 0 .
Step 5–6 — lock fails. lock() sees strong = 0 and returns an empty shared_ptr. So got_null == true. No dangling access ever happens.
Verify: expired == true and got_null == true. The check-and-use fusion protected us — this is why weak_ptr exists.
Worked example Why plain reference counting leaks a cycle
struct BadNode { std ::shared_ptr < BadNode > other; };
long leaked, fixed;
{
auto A = std :: make_shared < BadNode >(); // A strong = 1
auto B = std :: make_shared < BadNode >(); // B strong = 1
A->other = B; // B strong = 2
B->other = A; // A strong = 2
leaked = A. use_count (); // (1)
} // (2) A,B handles drop by 1 each → each strong = 1, NEVER 0
Forecast: After the block, does either object get deleted? What is A.use_count() at line (1)?
Step 1 — the counts. A->other = B gives B a second owner (strong 2 ); B->other = A gives A a second owner. So at line (1), leaked = 2.
Why this step? Each object now holds a strong handle to the other — look at the two burnt-orange arrows in the figure, each carrying a + 1 .
Step 2 — leaving scope. The local A and B die, each dropping one strong count: A goes 2 → 1 , B goes 2 → 1 . But each is still kept at 1 by the other object . The count limits to 1 , never 0 → neither is ever deleted . Classic leak.
The fix. Make one direction weak_ptr (deep-teal dotted arrow in the figure). A weak edge adds 0 to the strong count, so when the handles drop, one side hits 0 , deletes, which drops the other to 0 . fixed = 1 at the same point, and both free cleanly.
Verify: with two strong edges leaked == 2 and nothing frees. With one weak edge fixed == 1 and both objects free. This is exactly why Reference-counting needs weak_ptr (something a tracing garbage collector would handle differently).
Worked example A texture cache that must not keep textures alive
A game loads textures . Live sprites own their textures (shared_ptr). A central cache wants to hand back the same texture if it's still in use, but must not keep a texture alive after every sprite dropped it. Model the count.
std ::weak_ptr <int> cache; // cache observes only
long while_used, after_drop;
{
auto sprite = std :: make_shared < int >( 1 ); // (1) texture, strong = 1
cache = sprite; // (2) cache observes, strong still 1
while_used = sprite. use_count (); // (3)
} // (4) sprite gone → strong 0 → texture freed
after_drop = cache. expired () ? 0 : 1 ; // (5)
Forecast: If the cache held a shared_ptr, textures would live forever. Using weak_ptr, what is the count while a sprite uses it, and what does the cache report after?
Step 1–2 — load and register. The sprite owns the texture (strong 1 ); the cache only observes via weak_ptr, so strong stays 1 .
Why weak here? The cache's job is "give me the object if it still exists " — ownership belongs to sprites, not the cache. A shared_ptr cache would pin every texture forever (a leak).
Step 3 — measure. while_used == 1.
Step 4 — sprite leaves. Strong → 0 , texture freed immediately (good — no wasted memory).
Step 5 — cache asks later. cache.expired() is true, so after_drop == 0 — the cache correctly reports "gone" and would reload on next request.
Verify: while_used == 1 and after_drop == 0. The cache never inflated the count → no leak, textures die the instant nobody uses them. See Rule-of-Zero — the Node/cache structs need no hand-written destructor.
Worked example Count arithmetic under nested scopes and a move
auto a = std :: make_shared < int >( 0 ); // strong = 1
long r1, r2, r3, r4;
{
auto b = a; // strong = 2
{
auto c = a; // strong = 3
r1 = a. use_count (); // (A)
auto d = std :: move (c); // move, NOT copy → strong = 3 still
r2 = a. use_count (); // (B)
} // d dies (c was moved-from) → strong = 2
r3 = a. use_count (); // (C)
} // b dies → strong = 1
r4 = a. use_count (); // (D)
Forecast: Fill in r1, r2, r3, r4. The trap is line d = std::move(c) — does a move change the strong count?
Step A — r1. Owners a, b, c → r1 = 3.
Step B — the twist. std::move(c) transfers c's ownership to d. A move does not copy the control-block pointer into a new owner; it hands the existing ownership over and empties c. So the number of live owners is unchanged: r2 = 3.
Why this step? Moving a shared_ptr skips the strong++/strong-- pair — it's cheaper and count-neutral. Only copies and destructions of non-empty pointers move the count.
Step C — r3. Inner block ends: d dies (− 1 ); c is a moved-from empty pointer so its destruction does nothing . So 3 → 2 : r3 = 2.
Step D — r4. Outer block ends: b dies. r4 = 1. When a finally dies, strong hits 0 , the int is freed.
Verify: r1 == 3, r2 == 3, r3 == 2, r4 == 1. The move left the count flat at the (B) step — that's the whole trick.
Recall Rebuild the matrix from memory
Which cell is the "count limits to 1, never 0" leak? ::: Cell H — the reference cycle; fix with a weak_ptr on one edge.
Which cell is the degenerate empty input, and how do you use it safely? ::: Cell C — a default unique_ptr holds nullptr; guard with if (p) before *p.
Does copying a shared_ptr change the strong count? Does moving it? ::: Copy: +1. Move: unchanged (transfers the existing owner, empties the source).
Why do two shared_ptrs from one raw pointer double-free? ::: They build two independent control blocks; each count is 1 ; each deletes the same object.
Mnemonic The count's three verbs
Copy adds, Destroy subtracts, Move stays. Weak never touches the strong count — it only asks .