Smart Pointers
Ownership that deletes itself — when the object it owns goes out of scope, not when you remember to.
Intermediate
delete for you, automatically, the moment ownership ends.
That eliminates the two failure modes of raw new/delete: forgetting
to delete (a leak) and deleting more than once or deleting the wrong thing (a crash). Both
live in <memory>.
| unique_ptr | shared_ptr | weak_ptr | |
|---|---|---|---|
| Ownership | exclusive — exactly one owner | shared — a reference count tracks how many owners exist | none — observes without owning |
| Copyable | no (movable only) | yes — copying bumps the ref count | yes |
| Overhead | none — same size/speed as a raw pointer | a control block with two atomic counters | shares the shared_ptr's control block |
| Use it when | this is the default — reach for unique_ptr unless you specifically need shared ownership | multiple parts of the code genuinely need to co-own an object's lifetime | breaking a cycle between shared_ptrs, or observing without extending lifetime |
make_unique is preferred over a bare new mainly for exception
safety: with f(unique_ptr<T>(new T), other_arg()), C++ doesn't guarantee
argument evaluation order, so if other_arg() throws after new T
already ran but before the unique_ptr constructor captures it, that memory
leaks. make_unique does construction and ownership capture in one step, so
there's no window for that to happen.
unique_ptr can't be copied, passing it to a function by value
won't compile — the compiler would need a copy constructor that doesn't exist. Two
options: pass by reference (the function borrows it, ownership doesn't change), or pass by
value with an explicit std::move (ownership actually transfers to the function,
and the caller's pointer becomes null).
void borrow(std::unique_ptr<Widget> &p) { /* p is still owned by the caller after this returns */ }
void take(std::unique_ptr<Widget> p) { /* p is destroyed when this function returns */ }
auto w = std::make_unique<Widget>();
borrow(w); // fine — w is unaffected
take(std::move(w)); // ownership transferred — w is now null in the caller
shared_ptr to the same object shares one control block holding two
counts: how many shared_ptrs currently own the object, and how many
weak_ptrs are observing it. Copying a shared_ptr increments the
owner count; destroying or resetting one decrements it. The object is deleted the instant
the owner count hits zero — not before, regardless of how many weak observers remain.
shared_ptr is comparatively expensive — it touches an
atomic counter, because the count has to stay correct even if two threads copy the same
shared_ptr concurrently. Pass by const& when a function only
needs to use the object, and reserve pass-by-value (which does copy it, deliberately)
for a function that needs to become a co-owner.
make_shared vs. plain new: make_shared allocates
the object and its control block together in one block, which is faster and more
cache-friendly — use it by default. The one time to prefer new instead is
when a weak_ptr will outlive the object: because the control block and the
object share one allocation, that whole block can't be freed until both the owner
count and the weak count reach zero. A long-lived weak_ptr can therefore keep a
large object's memory pinned after the object itself is already destructed. Allocating with
new instead keeps the object and control block as two separate allocations, so
the object's memory is freed as soon as the owner count hits zero.
weak_ptr observes an object owned by a shared_ptr without
being an owner itself — it doesn't keep the object alive, and it knows when the object
is gone. You can't dereference a weak_ptr directly; call .lock()
first, which returns a shared_ptr that's either valid (and briefly extends the
object's lifetime while you use it) or null (if the object is already gone).
shared_ptrs to each other never reach a zero owner count — each keeps the
other's count above zero forever, leaking both. Making one side of the relationship a
weak_ptr breaks the cycle without changing which object logically owns which.
TopNotchNote/cpp/smart_pointers_weak_cycle.cpp
unique_ptr can't be copied, a range-based loop over
std::vector<std::unique_ptr<T>> has to bind by reference — the
default copy-binding a range-for otherwise uses would try to copy each element and fail to
compile.
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());
for (const auto &w : widgets) { w->draw(); } // by reference — required
// for (auto w : widgets) { ... } // error: unique_ptr copy constructor is deleted
std::move) that makes unique_ptr ownership transfer possible.