5.2.12 · D2C++ Programming

Visual walkthrough — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)

2,205 words10 min readBack to topic

Step 1 — What a pointer actually is (the ground floor)

WHAT. A raw pointer is a variable whose value is an address — a number naming a slot in memory. Written T* (read "pointer to a T"). The slot it names holds the real object.

WHY start here. Every smart pointer is built on top of a raw pointer. If you don't see the box-and-arrow picture, nothing later makes sense. We are drawing the thing that all the counting will wrap around.

PICTURE. On the left, a small box labelled p sitting on the stack (fast, automatic memory). Its value is an arrow pointing at a bigger box on the heap (memory you asked for with new) holding the number 7.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)


Step 2 — Bolt a counter onto the object: the control block

WHAT. Instead of the raw pointer standing alone, we attach a second heap box next to the object. That second box is the control block. For now it holds one integer we call strong = "how many owners are there right now?"

WHY this and not just a pointer. With a single owner (that was unique_ptr) you never need a count — one owner, one delete. But once we want many owners of the same object, someone has to remember how many there are, so the last one leaving knows to switch off the lights. A plain pointer cannot store that number; the control block is exactly the place to keep it.

PICTURE. The object box 7 on the heap, and beside it a control-block box with a field strong = 1. A shared_ptr named a is drawn as two arrows: one to the object, one to the control block.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)

Step 3 — Copy makes the count go UP

WHAT. Write auto b = a;. This is a copy, not a move. Both a and b now point at the same object and the same control block. The copy constructor does one extra thing: strong++.

WHY ++ on copy. A copy means "one more independent owner now exists." An owner is a promise: "I will keep this object alive as long as I live." Two promises ⇒ the tally must read 2, otherwise the object could be deleted while b still expects it (a dangling pointer).

PICTURE. Now two stack boxes a and b, each firing two arrows into the same object and same control block. The field flips strong = 1 → 2, drawn with a small "+1" tag on the copy arrow.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)

Step 4 — Scope exit makes the count go DOWN

WHAT. Let b live inside an inner { } block. When that block ends, b's destructor runs and does strong--. The object is not deleted yet, because a still holds it.

WHY -- on destruction, and why not delete yet. A destroyed owner means "one promise withdrawn." The tally drops. But deletion is only correct when no promises remain. Deleting at strong = 1 would betray a. So the destructor's logic is: decrement first, then check "am I the last one?"

PICTURE. The inner block's closing brace } highlighted; b's box fades out; a "−1" tag on the vanishing arrow; the field slides strong = 2 → 1. The object box 7 is still solid and alive.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)

Step 5 — The count hits ZERO: deletion fires

WHAT. Back in the outer scope, only a remains with strong = 1. When a's own block ends, its destructor does strong--, giving strong = 0, so the guard fires and the object 7 is deleted. The control block is then also freed (there are no observers — see Step 7).

WHY this is the "central result." This exact moment — the last owner leaving drives the count to 0 and triggers deletion — is the entire promise of shared_ptr. No human wrote delete; the arithmetic did.

PICTURE. a's box fades; strong = 1 → 0; a burst marks the object box being freed (it turns to a dashed outline). A timeline underneath shows the count over time: 1 → 2 → 1 → 0 → freed.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)
count reveal
The moment strong reaches 0, the managed object is deleted.
who deletes
The destructor of the last shared_ptr, via the if (strong == 0) guard.

Step 6 — The degenerate case: a CYCLE where the count never reaches 0

WHAT. Now the failure mode. Two objects A and B, each holding a shared_ptr to the other. Even after you drop your handles from the stack, A's control block still reads strong = 1 (because B points to it) and B's reads strong = 1 (because A points to it). Neither destructor ever fires. Both leak forever.

WHY the machine cannot escape. Deletion only happens inside a destructor. But a destructor only runs when a count hits 0. And each count is being propped up by the other object, which is itself waiting on the first. It is a stalemate — the arithmetic is self-sustaining. Reference counting has no idea this is a closed loop; it only ever looks at one number at a time.

PICTURE. Two object boxes A and B with a red arrow A → B and a red arrow B → A. The stack handles above are faded (already gone). Both control blocks stubbornly show strong = 1. A red "🔒 stuck" tag sits over the loop.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)

Step 7 — The fix: make ONE arrow non-counting (weak_ptr)

WHAT. Give the control block a second field, weak, and let one of the two links be a weak_ptr. A weak_ptr bumps weak, not strong. So make B → A weak: now A's strong counts only real owners.

WHY this precisely breaks the stalemate. In Step 6 both strong values were pinned at 1 by the mutual links. Convert one direction to weak and that direction stops adding to strong. When your outer handle to A drops, A's strong can now reach 0 → A is deleted → its shared_ptr to B is destroyed → B's strong reaches 0 → B is deleted. The dominoes fall.

PICTURE. Same two boxes, but B → A is now a dashed (weak) arrow tagged "weak++, strong unchanged". A's control block shows strong = 1, weak = 1. When the stack handle drops, A.strong → 0 and a domino sweep frees both.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)
why lock and not deref
Between "check alive" and "use", another owner could delete it; lock() fuses check-and-pin into one atomic step, so no dangling access.

The one-picture summary

Everything above compressed: the count as a rising/falling staircase driving deletion, the cycle as a flat line that never falls, and the weak arrow as the cut that lets it fall again.

Figure — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)
Recall Feynman retelling — the whole walkthrough in plain words

Picture a shared library book with a tally sheet clipped to it (that's the control block; the number on it is strong). Every person who borrows the book puts a tick on the sheet — that's a shared_ptr copy going strong++. Every person who returns it crosses one off — that's a destructor going strong--. The rule is dead simple: the person who crosses off the last tick puts the book back on the shelf — that's the if (strong == 0) delete guard firing. That is the entire trick: no single person is "in charge," yet the book disappears at exactly the right moment. Now the trap: two friends each scribble "the other one still has it" on the other's sheet, then both leave. Each sheet still shows a tick, so neither book is ever shelved — a leak, because the tally is local and can't see that the two references form a loop. The fix: let one friend write their note in erasable pencil that doesn't count as a tick — that's weak_ptr (weak++, strong untouched). Now one sheet can reach zero, that book gets shelved, which crosses off the last tick on the other sheet, and it gets shelved too. Before reading the penciled note you must first check the book is even still there — that's lock(). That's shared_ptr, cycles, and weak_ptr, entirely in pictures.


Related build-up: RAII · Move-semantics · Rule-of-Zero · Reference-counting · Garbage-collection · back to the parent topic.