Smart Pointers — ownership encoded in the type
Manual memory management in C++ looks simple — until early returns, exceptions, and branches show up. One missed delete is a leak; one extra one is UB; two owners of one object are a double free. The root cause is the same in every case: the resource and the responsibility for releasing it exist separately. One in the heap, the other in the developer's head.
Smart pointers solve this with RAII: a resource is acquired in a constructor and released in a destructor, and the destructor runs automatically on every path out of scope — through return, through an exception, through an early break. Responsibility moves out of documentation into the type.
The main thing a smart pointer buys you is not "auto-delete" — it is visible ownership in the signature:
std::unique_ptr<T>as a parameter is a contract: "transfer of ownership".const T&is "observation only; the object is alive for sure".T*orstd::weak_ptr<T>is "may be null or dead — check first".
Changing the type changes the meaning of the interface. The full map of ownership models lives in the layers below.
Topic map
- std::unique_ptr — single owner — zero overhead, move-only, custom deleter,
releasevsreset. - std::shared_ptr and the control block — shared ownership, control block,
make_sharedvsnew, counter vs object thread-safety. - std::weak_ptr — non-owning observer —
lockvsexpired, breakingshared_ptrcycles, the async-callback pattern. - enable_shared_from_this and self-shared_ptr — why
shared_ptr<T>(this)is UB, how a method returns ashared_ptrto itself. - Custom deleters and RAII for non-memory —
FILE*, OS handles,malloc/free,std::out_ptrfrom C++23. - boost::intrusive_ptr and intrusive counting — counter inside the object, COM/Qt/Unreal, differences from
shared_ptr.
Quick comparison
unique_ptr | shared_ptr | weak_ptr | intrusive_ptr | Raw T* | |
|---|---|---|---|---|---|
| Ownership | Single | Shared | None | Shared | None |
| Copying | No (move-only) | Yes | Yes | Yes | Yes |
| Control block | No | Yes | Yes (via shared_ptr) | No — counter in object | No |
| Overhead | ~zero | Atomic counters | Atomic on lock | Atomic counters | None |
| Primary use | Default owner | Shared lifetime | Observer / cycle break | Legacy / framework | Observation, interop |
Choice rule: default to unique_ptr. Reach for shared_ptr only when shared ownership is genuinely part of the domain model — not a way to avoid thinking about who deletes.
Common traps
| Mistake | Consequence |
|---|---|
shared_ptr<T>(this) inside a method | Two independent control blocks → double free, UB |
Two shared_ptrs from the same raw pointer | Same trap — two control blocks, double free |
shared_ptr cycle (Parent ↔ Child) | Leak — strong count never reaches 0 |
Capturing shared_ptr into an async callback lambda | Object lives as long as the callback — unexpected lifetime |
Treating shared_ptr as thread-safe for the object | Only the counter is atomic; the object still needs synchronization |
release() without an immediate delete | Leak — unique_ptr dropped ownership and nothing picked it up |
shared_from_this() on an object not yet managed | std::bad_weak_ptr (C++17) or UB (before) |
delete on malloc-ed memory via the default deleter | UB — needs a custom deleter |
unique_ptr<T> on a new T[] | UB on delete; you need unique_ptr<T[]> |
Liveness checks through use_count() | Fragile and race-prone; use weak_ptr::lock |
Interview relevance
Smart pointers are among the most-asked topics on C++ interviews, from junior to staff. Candidates who only know "the smart pointer auto-deletes" usually fall over on the first follow-up.
Typical checks:
- Understanding ownership models: unique vs shared vs observing.
- What the control block is — what it stores, where it lives, when it dies.
make_sharedvsshared_ptr(new T)— allocations, exception safety, the memory caveat with long-livedweak_ptrs.shared_ptrcycles — the classic gotcha.weak_ptr:lock()vsexpired(), the callback pattern.enable_shared_from_this: when you need it, what happens without it.- Thread safety: counter is atomic, the object is not.
- When custom deleters and
intrusive_ptrare appropriate.
Common wrong answer: "Smart pointers prevent all memory leaks." The accurate version: they prevent leaks if there are no shared_ptr cycles and if ownership is actually expressed in the type. The Parent/Child cycle is not a compiler bug — it is an ownership design bug that smart pointers do not fix on their own.