Skip to content

← References, Pointers, Ownership step 4 of 4

Hard Primitives

Two owners, and neither one ever leaves

std::shared_ptr is where people go when ownership is unclear, and unclear ownership is the one thing it does not fix. It is a precise tool for a narrow case:

Use shared_ptr when the object’s lifetime genuinely cannot be known at compile time — when the last of several independent owners, whichever it turns out to be, must be the one that destroys it.

If you can name the owner, use unique_ptr and hand out T* or T& to everyone else. That is most code.

What it costs

unique_ptr shared_ptr
Size one pointer two pointers
Construction none a control-block allocation
Copy deleted an atomic increment
Destruction a destructor call an atomic decrement, then maybe destruction

The atomics are the part that surprises people. They are paid whether or not your program has threads, they do not optimise away, and they serialise cores that touch the same count. A function taking shared_ptr<T> by value pays two of them per call for no ownership it actually keeps — which is why the rule is:

void inspect(const Widget& w);              // best: I only look
void inspect(const std::shared_ptr<Widget>& p);  // acceptable: may be null
void inspect(std::shared_ptr<Widget> p);    // an atomic pair, wasted

Pass the object unless the callee might extend its life.

The failure mode a reference count cannot see

Reference counting cannot collect cycles. If A holds a shared_ptr to B and B holds one back to A, each count stays at one after every external owner has gone. Neither is destroyed, the memory is unreachable, and no destructor ever runs — so anything either of them owned leaks too.

Trees are where this happens, because a parent link is such an obvious thing to add:

struct Node {
    std::vector<std::shared_ptr<Node>> children;   // parent owns children
    std::shared_ptr<Node> parent;                  // ...and children own parent
};

Nothing is wrong with either line on its own.

weak_ptr is how you say “I am watching this”

A std::weak_ptr refers to an object without owning it and without keeping it alive. It is not dereferenceable — you must ask for a real owner first:

std::shared_ptr<Node> p = weak.lock();   // null if the object is gone
if (p != nullptr) { ... }

That lock() is the whole interface, and its null case is not a formality: between your check and your use, the last owner may have released it, and holding the shared_ptr is what stops that happening.

So: the owning direction is shared_ptr, the back-reference is weak_ptr. Here, children are owned by their parent, and the parent link points back — so the parent link is the weak one.

One more note on make_shared

std::make_shared<T>(...) puts the control block and the object in one allocation instead of two. Prefer it — with one caveat worth knowing: that single block cannot be freed until the last weak_ptr is gone too, so a large object watched by long-lived weak_ptrs keeps its bytes around after its destructor has run.

Your task

depths is given: it builds a tree from a parent list and returns each node’s depth. Do not change it.

Change Node so the tree does not leak. Node counts how many instances are alive and the harness reads that count after depths has returned and every local has been destroyed — so it must be zero.

You may change the type of the parent member and the body of parent(). Nothing else needs to move.