5.2.12 · D5C++ Programming
Question bank — Smart pointers — unique_ptr (sole ownership), shared_ptr (shared ownership, ref count), weak_ptr (break cycles)
This page builds on ideas of RAII, Reference-counting, Move-semantics, Rule-of-Zero, and Memory-leaks-and-dangling-pointers. Nothing here is computation-heavy — it is all reasoning.


True or false — justify
A unique_ptr costs more memory than a raw pointer
False — with the default deleter it holds exactly one pointer and stores no deleter state, so
sizeof(unique_ptr<T>) equals sizeof(T*). This zero-overhead guarantee holds only for the default (stateless) deleter — see the stateful-deleter question below.A unique_ptr<T, D> with a stateful custom deleter D is still the size of a bare pointer
False — a stateful deleter (e.g. a lambda that captures, or a
std::function) must be stored inside the unique_ptr, so its size grows by the deleter's footprint. Only stateless/default deleters keep it pointer-sized (empty base optimization).A shared_ptr is the same size as a raw pointer
False — it holds two pointers: one to the object and one to the control block, so it is typically twice the size of a raw pointer.
Copying a shared_ptr copies the managed object
False — it only copies the two pointers and increments the strong count; both copies point at the same object. Copying is cheap but shares the object.
weak_ptr keeps the object alive as long as it exists
False — a
weak_ptr never touches the strong count, so it never keeps the object alive; that is its whole purpose. It only observes.Moving a unique_ptr leaves the source usable
False — after
std::move, the source is set to nullptr; using it as if it still owns an object is a logic error. Move transfers, it does not duplicate.make_shared allocates the object and control block together
True — a single allocation holds both, which is faster and cache-friendly; the two-step
shared_ptr<T>(new T) allocates twice.A shared_ptr with use_count() == 1 is the last owner
True — the strong count is exactly the number of shared owners, so
1 means one owner remains and destroying it will delete the object.weak_ptr has a use_count() that includes itself
False —
use_count() on a weak_ptr reports the strong count (owners), and weak references are counted separately; a live weak pointer with all owners gone reports 0.unique_ptr can be stored in a std::vector
True — vectors only require movable elements, and
unique_ptr is movable; you just cannot copy the vector.Two unique_ptrs can point to the same object safely
False — if both existed and owned the same object, both destructors would
delete it → double-free. This is exactly why copy is =deleted.An aliasing shared_ptr and its target always point to the same address
False — the aliasing constructor
shared_ptr<int>(owner, &owner->member) lets the stored pointer point at a sub-object (a member) while the control block keeps the whole owner alive. The stored pointer and the owned object can differ — see the aliasing example below.Spot the error
shared_ptr<T> a(raw); shared_ptr<T> b(raw); where T* raw = new T(); — what breaks?
Two separate control blocks are created from the same raw pointer (look at Figure 2), so each thinks it is the sole owner → double-free. Make one
shared_ptr and copy it instead.struct Bad {
std::shared_ptr<Bad> makeHandle() {
return std::shared_ptr<Bad>(this); // ⚠️ trap
}
};Why does returning shared_ptr<Bad>(this) cause a double-free?
It builds a brand-new control block around
this, unaware of the one that already owns the object, so two blocks each delete it (Figure 2). Fix: inherit std::enable_shared_from_this<Bad> and return shared_from_this();, which reuses the existing control block.std::shared_ptr<int> sp = std::make_shared<int>(5);
std::weak_ptr<int> w = sp;
std::cout << *w; // ⚠️Why won't *w compile?
weak_ptr has no operator* or operator-> because the object might already be gone; you must w.lock() to get a shared_ptr first, then check it.std::unique_ptr<int> p = std::make_unique<int>(9);
auto q = p; // ⚠️Why is auto q = p; a compile error?
The copy constructor of
unique_ptr is deleted to prevent double ownership; you need auto q = std::move(p); to transfer instead (after which p is nullptr).std::weak_ptr<int> w = someShared;
if (!w.expired()) std::cout << *w.lock(); // ⚠️What's still risky about this naive expired() check?
Between
expired() returning false and using the object, another thread could drop the last owner; the correct pattern is to lock() once into a variable and check that returned shared_ptr, which atomically pins it.A Node with shared_ptr<Node> next and shared_ptr<Node> prev — what's the bug?
The two
shared_ptrs form a cycle whose strong count never reaches zero, leaking both nodes; make one direction (prev) a weak_ptr so it observes without counting.std::unique_ptr<int> p(new int[10]); — what's wrong with the deletion?
The default deleter calls scalar
delete, but an array needs delete[]; use unique_ptr<int[]> (or better, std::vector) so the array-deleter runs.std::unique_ptr<Widget> make() {
auto p = std::make_unique<Widget>();
return std::move(p); // harmless but unneeded
}What is the harmless-but-unneeded part?
The explicit
std::move is redundant (and can disable copy elision); a local unique_ptr returned by value is already moved automatically — return p; is enough.Why questions
Why is unique_ptr non-copyable but movable?
Copying would create two owners of one object → double-free, so copy is forbidden; moving transfers the single ownership and empties the source, keeping the "exactly one owner" invariant.
Why does shared_ptr need a separate control block instead of storing the count in the object?
The count must survive even for objects you don't control (e.g.
shared_ptr<int>), and multiple shared_ptrs must reach the same counter; a shared external control block gives one canonical count (the red box in Figure 1).Why must a member function that hands out a shared_ptr to itself use enable_shared_from_this?
Constructing
shared_ptr<T>(this) invents a second control block; shared_from_this() instead locates the existing one (stored via a hidden weak_ptr in the base), so all owners share a single count.Why must a cycle-breaking back-reference be weak_ptr rather than a raw pointer?
A raw pointer would dangle when the parent dies with no way to detect it;
weak_ptr::lock() safely tells you whether the target is still alive before you touch it.Why prefer make_unique / make_shared over new?
They keep the raw
new from ever being visible, are exception-safe (no leak if a nearby argument throws), and make_shared fuses two allocations into one.Why does the control block outlive the object when weak pointers remain?
The object is destroyed at
strong == 0, but the block stores the weak count too; weak pointers need it to answer "is the object gone?", so the block is freed only at strong == 0 && weak == 0.Why is unique_ptr the recommended default over shared_ptr?
Sole ownership is cheaper (one pointer, no atomic counter), clearer about who is responsible, and most designs genuinely have a single owner — reach for sharing only when lifetime is truly shared.
Why does incrementing a shared_ptr's count need to be atomic?
Multiple threads may copy or destroy shared owners simultaneously; non-atomic
++/-- could lose updates and delete too early or leak, so the strong count uses atomic operations.Aliasing constructor — a worked warning
Edge cases
What is use_count() of a default-constructed shared_ptr (empty)?
It is
0 — there is no object and no owner, so the strong count is zero and dereferencing it is undefined behaviour.What does w.lock() return when the object was already deleted?
An empty
shared_ptr (evaluates to false), so if (auto s = w.lock()) cleanly takes the "expired" branch instead of dangling.Can a shared_ptr manage a nullptr yet still have a control block?
Yes —
shared_ptr<T>(nullptr, deleter) and aliasing constructors can give a null stored pointer with a live control block; the count still behaves normally, it just manages nothing to delete.With an aliasing shared_ptr to a member, when is the whole owner object destroyed?
Only when the combined strong count of the owner and all aliasing copies reaches zero; an alias to one field keeps the entire parent alive, which can look like a "leak" if you forget the alias exists.
What happens if you reset() the only shared_ptr to an object with live weak_ptrs?
The strong count hits
0 so the object is destroyed immediately, but the control block stays until the last weak_ptr dies; those weak pointers then report expired.Is a moved-from unique_ptr safe to reassign later?
Yes — a moved-from
unique_ptr is a valid empty (nullptr) object; you may reassign, reset, or let it die harmlessly. Only dereferencing the empty pointer is the error.Does destroying a shared_ptr inside a cycle (all handles dropped) free memory if one link is weak?
Yes — the weak link doesn't count, so the remaining strong chain can reach
0, triggering destruction of both nodes; the cycle is broken precisely because one edge is non-owning.What ownership model does a shared_ptr copied across threads guarantee about deletion order?
The object is deleted exactly once, by whichever thread drops the last strong reference; the atomic count ensures no double-free regardless of thread scheduling.
Recall One-line summary of the traps
Never build two control blocks from one object (raw pointer or this without enable_shared_from_this); never deref a weak_ptr without lock(); never copy a unique_ptr; break every ownership cycle with exactly one weak_ptr; remember a stateful deleter grows unique_ptr; and an aliasing shared_ptr keeps the whole owner alive even when it points at just one member.