C++ Smart Pointers: unique, shared, weak
A practical guide to unique_ptr, shared_ptr, and weak_ptr in modern C++: ownership semantics, when to use each, and the pitfalls that lead to leaks and cycles.
What you'll learn
- ✓What ownership means in modern C++
- ✓When to use unique_ptr vs shared_ptr
- ✓How weak_ptr breaks reference cycles
- ✓Custom deleters and aliasing constructors
- ✓Common pitfalls and how to avoid them
Prerequisites
- •Basic familiarity with the language
Modern C++ removed most reasons to write new and delete by hand. The standard library offers three smart pointers that cover almost every ownership pattern: unique_ptr, shared_ptr, and weak_ptr. They are not interchangeable. Pick the wrong one and you get either subtle leaks or unnecessary overhead.
Why smart pointers exist
A raw pointer says nothing about who owns the object it points at. Reading a function signature like void process(Widget* w) you cannot tell whether process should delete w, store it, or simply read from it. Memory bugs in C++ almost always come from this ambiguity.
Smart pointers encode ownership in the type system. A function that takes a unique_ptr<Widget> by value is saying, in compiler-enforced terms, that it is taking ownership.
unique_ptr
A unique_ptr<T> owns one object. It cannot be copied, only moved. When it goes out of scope, it deletes the object. Zero overhead compared to a raw pointer plus manual delete.
#include <memory>
auto w = std::make_unique<Widget>(42);
w->doSomething();
// w deletes the Widget automatically when it goes out of scope
Pass it by value to transfer ownership; pass the underlying reference (Widget&) when you only need to use it.
void consume(std::unique_ptr<Widget> w); // takes ownership
void inspect(const Widget& w); // borrows
Use unique_ptr whenever you have a single, clear owner. That is the vast majority of cases.
shared_ptr
A shared_ptr<T> owns an object jointly with other shared_ptr instances. Each copy increments a reference count, each destruction decrements it. The object is deleted when the count hits zero.
auto a = std::make_shared<Widget>(42);
auto b = a; // ref count = 2
// both a and b destroyed -> Widget deleted
shared_ptr is not free. It carries a control block with reference counts (strong and weak), and the increment/decrement is atomic for thread safety. For high-frequency code, that overhead matters.
Use shared_ptr only when ownership is genuinely shared and the lifetime cannot be expressed as a tree. Otherwise, unique_ptr is better.
weak_ptr
A weak_ptr<T> observes a shared_ptr without contributing to its reference count. You cannot dereference it directly; you must call lock() to get a shared_ptr, which is null if the object has already been destroyed.
std::shared_ptr<Node> root = std::make_shared<Node>();
std::weak_ptr<Node> obs = root;
if (auto live = obs.lock()) {
live->useIt();
} else {
// already gone
}
The main reason to reach for weak_ptr is breaking reference cycles, especially in graphs and observer patterns.
The mental model
Think of the three as different relationships to the lifetime of an object.
unique_ptr : single owner ----[T]
shared_ptr : co-owners ====[T]====
^
weak_ptr : observer (no own) - - + The ==== lines stand for strong references that keep the object alive. The dashed line is non-owning.
Reference cycles
shared_ptr cannot detect cycles. Two objects holding shared_ptr to each other will never be deleted.
struct Node {
std::shared_ptr<Node> peer; // BUG: forms a cycle
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->peer = b;
b->peer = a; // leak when a and b go out of scope
The fix: one direction is strong, the other is weak.
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
};
A common convention is parent owns child via shared_ptr (or unique_ptr), and child observes parent via weak_ptr or a raw pointer.
Custom deleters
Sometimes you wrap a resource that is not freed with delete: a C API handle, a file descriptor, a CUDA buffer. Smart pointers accept a custom deleter.
auto file = std::unique_ptr<FILE, decltype(&std::fclose)>(
std::fopen("data.txt", "r"), &std::fclose);
For shared_ptr, the deleter type does not become part of the smart pointer type, so heterogeneous deleters are easier. For unique_ptr, the deleter is in the type and lambdas or function pointers work.
Common pitfalls
Calling make_shared when you only need one owner. You pay for an atomic refcount you never use.
Storing this as a shared_ptr directly. The constructor does not know there is already a shared_ptr to the object, so you create a second control block. Use std::enable_shared_from_this.
Mixing raw new with make_shared. Always prefer make_unique and make_shared. They are exception-safe and avoid an extra allocation for the control block in the shared case.
Passing shared_ptr by value when by reference would do. If a function does not need to extend lifetime, take a const reference or a raw pointer/reference to the underlying type.
Practical tips
Default to unique_ptr. Move it where ownership changes. Reach for shared_ptr only when multiple owners are unavoidable. Use weak_ptr whenever you need to observe but not own. Treat raw pointers and references as non-owning borrows; they are still useful, just not for ownership.
Wrap-up
Smart pointers are not about avoiding delete. They are about making ownership visible. Once your function signatures honestly say who owns what, memory bugs become a matter of reading the types. Pick unique_ptr first, escalate only when necessary, and use weak_ptr to break cycles when shared ownership leads to them.
Related articles
- C++ C++ RAII Resource Management Explained
RAII is the central idea behind safe C++ code. Learn how scope-bound resource ownership eliminates leaks, simplifies error handling, and scales beyond memory.
- C++ C++ References vs Pointers Explained
A clear comparison of references and pointers in C++, when each is the right tool, and how their semantics interact with const, ownership, and APIs.
- C++ Modern C++ Smart Pointers: unique_ptr and shared_ptr
Use unique_ptr, shared_ptr, and weak_ptr to model ownership in modern C++ without manual new and delete or memory leaks.
- Rust Rust Smart Pointers: Box, Rc, and Arc
Understand when to reach for Box, Rc, and Arc in Rust, how each interacts with the borrow checker, and the cost of shared ownership across threads.