Exercises — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)
Before we start, one picture to fix vocabulary. Every shared_ptr carries two arrows: one to the object and one to a small bookkeeping struct called the control block which stores two integers — the strong count (real owners) and the weak count (observers).

Read this figure now: the blue arrows are the shared_ptrs pointing at the object; the yellow box is the control block; strong is the number of blue owners, weak is the number of green (non-owning) observers. The object is deleted the instant strong hits 0. The control block itself is freed only when both strong == 0 and weak == 0.
Level 1 — Recognition
Recall Solution L1.1
(a) ==unique_ptr — sole ownership, cheapest, "unique by default."
(b) weak_ptr — non-owning observer; does not raise the strong count.
(c) shared_ptr== — shared lifetime with a reference count.
Recall Solution L1.2
use_count() reports the strong count. Line (1) sets it to 1; each copy (b=a, c=b) is +1. So after line (3) the count is . All three names point at the same object and the same control block.
Answer: 3.
Level 2 — Application
Recall Solution L2.1
Track strong line by line:
- A:
strong = 1(justa). - B: copy →
strong = 2(a,b). - C: copy →
strong = 3(a,b,c). - D:
cleaves inner scope →strong = 2. - E prints
2. - F:
bleaves scope →strong = 1. - G prints
1. After G,adies at end of program →strong = 0→ theintis deleted exactly once.
Recall Solution L2.2
Wrap the resource in a unique_ptr; its destructor runs on every exit path — normal return, early return, or a thrown exception. This is exactly RAII.
void f() {
auto p = std::make_unique<Widget>();
if (broken()) return; // p's destructor runs -> no leak
use(p.get()); // pass the raw pointer without giving up ownership
} // p's destructor runs here tooNo hand-written delete survives, so leaks become impossible on this path.
Level 3 — Analysis
Recall Solution L3.1
Constructing a shared_ptr from a raw pointer creates a brand-new control block. Doing it twice from the same raw makes two independent control blocks, each with strong = 1.
a.use_count()reports1.b.use_count()reports1. Neither knows about the other. Whenadies it deletes theWidget(its count →0); whenbdies it deletes the same, already-freedWidgetagain → double-free → undefined behaviour / crash. Fix: create oneshared_ptrand copy it, so both share one control block:
auto a = std::make_shared<Widget>();
auto b = a; // strong = 2, one control blockRecall Solution L3.2
Copying a shared_ptr copies both pointers verbatim, then does cb->strong++. So:
- distinct
ptrvalues:1(both aim at the same object). - distinct
cbvalues:1(both aim at the same control block — that is why the count is shared). Contrast with L3.1: two raw-pointer constructions → still1distinctptr, but2distinctcb— the source of the bug. See Reference-counting.
Level 4 — Synthesis
Recall Solution L4.1
Follow the strong counts. Look at the figure below.

- After
make_shared: object-1strong = 1(namen1), object-2strong = 1(namen2). n1->next = n2;copies into object-1's field → object-2strong = 2.n2->prev = n1;copies into object-2's field → object-1strong = 2.n1,n2leave scope → each drops by1: object-1strong = 1, object-2strong = 1.
(a) Object-1's strong count is 1 (never 0) — it is held alive by object-2's prev field, which is held alive by object-1's next field, and so on. A mutual, self-sustaining loop.
(b) No — neither is freed. Both leak forever. Plain Reference-counting cannot collect cycles (unlike tracing Garbage-collection).
(c) Make one back-pointer non-owning: change prev to std::weak_ptr<Node>. A weak_ptr does not bump the strong count, so after n1, n2 leave scope both counts reach 0 and both objects are freed.
Recall Solution L4.2
n1->next = n2;→ object-2strong = 2(as before;nextis still owning).n2->prev = n1;→ object-1strongunchanged at1; the weak assignment bumps only the weak count (to1).n1,n2leave scope: object-1strong = 0→ object-1 deleted. Deleting object-1 destroys itsnextfield → object-2strong = 1... then the namen2was already gone, so object-2strong = 0→ object-2 deleted. Both freed, no leak. Final strong counts: object-1 =0, object-2 =0.
Level 5 — Mastery
Recall Solution L5.1
- B:
wobservessbut adds0to the strong count. - C:
w.use_count()reports the strong count of the managed object, which is still justs→ prints1. - D:
s.reset()drops the last strong owner →strong = 0→ theintis deleted. (wis now expired; the object is gone but the control block survives becauseweak = 1.) - E:
w.lock()tries to promote to ashared_ptr. Sincestrongis already0, it cannot revive the object → returns an emptyshared_ptr→ifis false. - The
elseruns → printsexpired. This is why welock()instead of dereferencing aweak_ptrdirectly: between "is it alive?" and "use it," another thread could reset the last owner.lock()atomically either pins the object or reports it gone — no dangling read.
Recall Solution L5.2
Ownership flows down the tree (parent owns children), and the back-pointer flows up (child sees parent). So:
struct Node {
std::vector<std::shared_ptr<Node>> children; // owns downward
std::weak_ptr<Node> parent; // observes upward
};- Why
shared_ptrfor children? A parent genuinely co-owns its children (and other structures may too). - Why
weak_ptrfor parent? If it wereshared_ptr, parent↔child would form a cycle → strong count of the parent never hits0→ leak (exactly L4.1). Weak adds0to the strong count, so when the parent's last owner dies its count reaches0and the whole subtree tears down. - Rule-of-Zero: because every member is a smart pointer (or a container of them), the compiler-generated destructor, copy, and move already do the right thing. You write no destructor, no
delete, no manual copy/move — "zero" special members. RAII plus smart pointers gives correct cleanup for free.
A child reaches its parent safely with if (auto p = parent.lock()) { ... }.
Recall One-line self-check
Own forward with shared_ptr, observe backward with weak_ptr, and reach for unique_ptr first ::: because sole ownership is cheapest and clearest; escalate to shared/weak only when lifetime is genuinely shared or you must break a cycle.