Which Smart Pointer?
Decision guide for choosing between unique_ptr, shared_ptr, weak_ptr, and raw pointers in C++.
Quick Answer
Do you need heap allocation?
No → Use a local variable (stack) or a value member.
Yes ↓
Is ownership shared between multiple owners?
No → std::unique_ptr ← default choice
Yes ↓
Do all owners have equal lifetime?
Yes → std::shared_ptr
No → shared_ptr for owners + weak_ptr for observersThe full decision tree
Step 1: Do you need a pointer at all?
Most C++ objects should be local variables or value members:
// Don't do this:
auto w = std::make_unique<Widget>();
// Do this (unless Widget is large or polymorphic):
Widget w;Use smart pointers when:
- The object's lifetime must outlast its enclosing scope
- You need polymorphism (base pointer to derived)
- The object is very large and you're passing it around
- Ownership needs to be transferred
Step 2: Who owns the object?
Exclusive ownership → std::unique_ptr
One owner. When the owner is destroyed, the object is destroyed.
auto w = std::make_unique<Widget>(); // I own this exclusively
// Transfer ownership:
void sink(std::unique_ptr<Widget> w); // takes ownership
sink(std::move(w)); // w is now nullShared ownership → std::shared_ptr
Multiple owners. Object lives until the last owner is released.
auto w = std::make_shared<Widget>(); // shared ownership
auto copy = w; // both own it — ref count 2
// Object deleted when both w and copy are destroyedNon-owning observation → std::weak_ptr
Observe a shared_ptr-managed object without extending its lifetime.
std::weak_ptr<Widget> observer = w; // doesn't extend lifetime
if (auto locked = observer.lock()) { // safe access — may be null
locked->doWork();
}Non-owning, lifetime guaranteed → raw pointer or reference
When you can guarantee the pointed-to object outlives the pointer:
void process(Widget* w); // non-owning, not nullable
void process(Widget& w); // preferred — non-owning, not nullComparison table
unique_ptr | shared_ptr | weak_ptr | raw ptr/ref | |
|---|---|---|---|---|
| Owns object | Yes | Yes (shared) | No | No |
| Copyable | No | Yes | Yes | Yes |
| Thread-safe ref count | — | Yes | Yes | — |
| Overhead | None | Control block | Control block | None |
| Nullable | Yes | Yes | Yes | Pointer: yes, Ref: no |
| Use for | Default | Shared lifetime | Back-refs, caches | Borrowed access |
Common scenarios
Factory functions
// Return unique_ptr — caller can convert to shared_ptr if needed
std::unique_ptr<Widget> createWidget();Observer / callback
// Store weak_ptr in callback; lock() before use
class Button {
std::vector<std::weak_ptr<Listener>> listeners;
public:
void click() {
for (auto& wl : listeners) {
if (auto l = wl.lock()) l->onClicked();
}
}
};Tree structure (parent/children)
struct Node {
std::vector<std::shared_ptr<Node>> children; // owns children
std::weak_ptr<Node> parent; // back-link, no cycle
};Cache
class Cache {
std::map<Key, std::weak_ptr<Value>> entries;
public:
std::shared_ptr<Value> get(Key k) {
auto& wp = entries[k];
if (auto v = wp.lock()) return v;
auto v = load(k);
wp = v;
return v;
}
};Custom deleters
unique_ptr accepts a custom deleter as its second template argument, making it suitable for managing any resource that has an acquire/release pair:
// FILE* with custom deleter:
auto file_deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_deleter)> f{
fopen("log.txt", "w"), file_deleter
};
// SDL_Window with function pointer deleter:
std::unique_ptr<SDL_Window, void(*)(SDL_Window*)> win{
SDL_CreateWindow("App", 0, 0, 800, 600, 0),
SDL_DestroyWindow
};
// Type-erased deleter via std::function (adds overhead):
std::unique_ptr<Resource, std::function<void(Resource*)>> r{
acquire(), [pool](Resource* p) { pool->release(p); }
};shared_ptr also accepts a custom deleter, but it stores it type-erased in the control block (no template parameter needed):
std::shared_ptr<SDL_Window> win{
SDL_CreateWindow("App", 0, 0, 800, 600, 0),
SDL_DestroyWindow // stored in control block, no extra template arg
};unique_ptr<T[]> — owning array
// For owning a raw array (prefer std::vector instead when possible):
auto buf = std::make_unique<char[]>(1024);
buf[0] = 'A'; // operator[] provided
// delete[] called automatically — NOT deleteUse unique_ptr<T[]> when you need a raw heap array with no resizing. For most cases, std::vector<T> is better.
enable_shared_from_this — getting a shared_ptr to this
If an object is managed by shared_ptr, and a member function needs to create a shared_ptr to this, derive from enable_shared_from_this:
#include <memory>
class Session : public std::enable_shared_from_this<Session> {
public:
void start() {
// shared_from_this() returns a shared_ptr to this — safe
auto self = shared_from_this();
async_read([self](auto& data) {
self->handle(data); // self keeps Session alive during the async op
});
}
};
// ONLY works if Session was originally created via shared_ptr:
auto s = std::make_shared<Session>();
s->start(); // safe
// This would throw std::bad_weak_ptr:
// Session raw;
// raw.start(); // shared_from_this() on non-shared_ptr-managed objectPerformance overhead
unique_ptr<T> | shared_ptr<T> | weak_ptr<T> | raw T* | |
|---|---|---|---|---|
sizeof | Same as T* (+ deleter if non-default) | 2× pointer (ptr + control block ptr) | 2× pointer | 1× pointer |
| Heap allocations | 1 (for the object) | 2 (object + control block) or 1 with make_shared | 0 | 0 |
| Dereference cost | Zero overhead | Zero overhead | Must lock() first | Zero overhead |
| Copy | Not copyable | Atomic increment | Atomic check | Trivial |
| Thread-safe ref count | N/A | Yes (atomic) | Yes (atomic) | N/A |
make_shared vs new+shared_ptr:
// Two allocations: one for Widget, one for the control block
std::shared_ptr<Widget> w1(new Widget(42));
// One allocation: Widget and control block in a single block
auto w2 = std::make_shared<Widget>(42); // prefer thisThe single-allocation form is faster and has better locality. Downside: the memory for Widget is not freed until the last weak_ptr expires (control block keeps the whole block alive).
Non-owning access: T* vs T& vs std::reference_wrapper
When you don't own the object, express that clearly:
// Best: reference (non-null, non-owning, semantically "borrow")
void process(Widget& w);
// OK: pointer (nullable, non-owning)
void maybe_process(Widget* w); // null means "nothing to do"
// For storing a non-owning reference in a container (references can't be stored):
std::vector<std::reference_wrapper<Widget>> widgets;
widgets.push_back(std::ref(w));
widgets[0].get().draw();Rule of thumb
Default to
unique_ptr. Upgrade toshared_ptronly when ownership is genuinely shared between independent owners. Useweak_ptrto break reference cycles or observe shared objects. Express non-owning access through raw references (T&) or pointers (T*) — never through a smart pointer that doesn't convey ownership.