Smart Pointers
unique_ptr, shared_ptr, weak_ptr — RAII for heap memory.
Raw new/delete are easy to get wrong. The standard library's
smart pointers are RAII wrappers that hold a heap pointer and
delete it automatically. Once you internalize them, you almost
never write delete again.
There are three to know:
std::unique_ptr<T>— exclusive owner. Lightweight, free of overhead. Use this first; it covers most cases.std::shared_ptr<T>— shared owner. Multiple shared_ptrs can hold the same object; the last one to die deletes it.std::weak_ptr<T>— non-owning observer of ashared_ptr. Used to break cycles or watch an object without keeping it alive.
std::unique_ptr: one owner, no overhead
std::make_unique<Widget>(1) is preferred over new Widget(1) for
two reasons: it's exception-safe in surprising ways, and you never
have to write the type twice.
A unique_ptr cannot be copied — only moved (next chapter).
That single restriction enforces "exactly one owner" at compile
time:
std::unique_ptr<Widget> a = std::make_unique<Widget>(1);
std::unique_ptr<Widget> b = a; // ERROR: copying not allowed
std::unique_ptr<Widget> c = std::move(a); // OK: ownership transferred
// a is now nullptr; c owns the widgetstd::shared_ptr: when ownership really is shared
shared_ptr keeps a small reference count alongside the object.
Each copy increments the count; each destruction decrements it.
When the count hits zero, the object is deleted.
Use shared_ptr only when you truly need shared ownership — a node
in a graph that several roots can reach, a cached object accessed
from multiple subsystems. Defaulting to shared_ptr "just in case"
is a common over-correction; the reference count costs CPU and
clouds ownership.
std::weak_ptr: observer that doesn't keep alive
A common shared-pointer trap: two objects each hold a shared_ptr
to the other. Neither's count ever hits zero, so neither is
destroyed — a cycle leak.
The fix is to make one of the directions a weak_ptr. A weak_ptr
does not increment the reference count; it lets you ask "is the
target still alive?" and, if so, briefly lock it into a
shared_ptr for safe use.
std::shared_ptr<Node> parent = std::make_shared<Node>();
std::weak_ptr<Node> peek = parent; // doesn't keep parent alive
if (auto p = peek.lock()) {
p->doSomething(); // safe — we have a real shared_ptr now
} else {
// the parent is gone
}We won't go deeper here. The thing to take away is that smart pointers express ownership intent: who keeps the object alive, who merely watches it.
Picking a smart pointer
A practical heuristic:
- Use
std::unique_ptrby default. - Reach for
std::shared_ptronly when ownership is genuinely shared. - Use raw pointers/references for non-owning views — function parameters that just read the object.
Smart pointer ≠ everywhere
For things that already manage their own storage (std::vector,
std::string, std::map, ...), you do not need smart pointers.
A std::vector<Widget> already owns its Widgets. Wrapping vectors
in smart pointers is almost always wrong.
Use smart pointers when you have a single polymorphic object whose
lifetime needs explicit management, e.g., std::unique_ptr<Shape>
in a heterogeneous container.
Challenge
Replace the raw new/delete in the provided main with std::make_unique and std::unique_ptr. The behaviour and output must be unchanged: prints Widget 1 created, then 123, then Widget 1 destroyed, each on its own line.
Test your understanding
Why is std::unique_ptr preferred over a raw new/delete pair?
It is faster than a raw pointer.
It allows multiple owners.
It applies RAII: the destructor runs on scope exit and reliably deletes the owned object, eliminating leaks, double-deletes, and use-after-free bugs on the owner side.
It is the only legal way to use the heap in modern C++.
When should you reach for std::shared_ptr instead of std::unique_ptr?
Whenever you allocate on the heap.
For all polymorphic objects.
When the lifetime of the object is genuinely shared across multiple owners that may release it in any order — and unique_ptr cannot capture that.
For any object larger than 64 bytes.
Next: the move in std::move(a) — what it actually does, and why
it makes returning heavy objects from functions cheap.