Skip to content
C++

Which Smart Pointer?

Decision guide for choosing between unique_ptr, shared_ptr, weak_ptr, and raw pointers in C++.

Quick Answer

cpp
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 observers

The full decision tree

Step 1: Do you need a pointer at all?

Most C++ objects should be local variables or value members:

cpp
// 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.

cpp
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 null

Shared ownership → std::shared_ptr

Multiple owners. Object lives until the last owner is released.

cpp
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 destroyed

Non-owning observation → std::weak_ptr

Observe a shared_ptr-managed object without extending its lifetime.

cpp
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:

cpp
void process(Widget* w);       // non-owning, not nullable
void process(Widget& w);       // preferred — non-owning, not null

Comparison table

unique_ptrshared_ptrweak_ptrraw ptr/ref
Owns objectYesYes (shared)NoNo
CopyableNoYesYesYes
Thread-safe ref count—YesYes—
OverheadNoneControl blockControl blockNone
NullableYesYesYesPointer: yes, Ref: no
Use forDefaultShared lifetimeBack-refs, cachesBorrowed access

Common scenarios

Factory functions

cpp
// Return unique_ptr — caller can convert to shared_ptr if needed
std::unique_ptr<Widget> createWidget();

Observer / callback

cpp
// 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)

cpp
struct Node {
    std::vector<std::shared_ptr<Node>> children;  // owns children
    std::weak_ptr<Node> parent;                   // back-link, no cycle
};

Cache

cpp
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:

cpp
// 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):

cpp
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

cpp
// 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 delete

Use 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:

cpp
#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 object

Performance overhead

unique_ptr<T>shared_ptr<T>weak_ptr<T>raw T*
sizeofSame as T* (+ deleter if non-default)2× pointer (ptr + control block ptr)2× pointer1× pointer
Heap allocations1 (for the object)2 (object + control block) or 1 with make_shared00
Dereference costZero overheadZero overheadMust lock() firstZero overhead
CopyNot copyableAtomic incrementAtomic checkTrivial
Thread-safe ref countN/AYes (atomic)Yes (atomic)N/A

make_shared vs new+shared_ptr:

cpp
// 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 this

The 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:

cpp
// 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 to shared_ptr only when ownership is genuinely shared between independent owners. Use weak_ptr to 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.