Skip to content
C++

Smart Pointers

Smart pointers are RAII wrappers around raw pointers that encode ownership in the type system. They eliminate the need to manually pair new with delete, and they make ownership decisions visible and enforceable at compile time.

Why raw pointers are not enough

A raw pointer tells you where an object is, but it tells you nothing about ownership. Given a raw pointer, you cannot answer six fundamental questions from the type alone:

1.Does this pointer own the object?
2.Should I delete it when I'm done?
3.Do I call delete or delete[]?
4.Is there exactly one owner, or are there many?
5.Could this pointer already be null?
6.Has the object already been destroyed?

Smart pointers answer these questions by design. The choice between unique_ptr, shared_ptr, and weak_ptr is a decision about ownership policy — and it is expressed in the type, not in comments.

unique_ptr — exclusive ownership

std::unique_ptr<T> represents exclusive ownership: at any moment, exactly one unique_ptr owns the object. When that pointer is destroyed (or reset), the object is destroyed with it. It is move-only — you can transfer ownership, but you cannot copy it, because that would create two owners for the same object.

With the default deleter, a unique_ptr is exactly the same size as a raw pointer — it adds zero overhead at runtime. This is the smart pointer to reach for first, because it is both the simplest and the fastest. Scott Meyers writes in Effective Modern C++ that unique_ptr "is the C++11 way to express exclusive ownership."

#include <memory>

// Always prefer make_unique over new — explained in the make section below
auto p = std::make_unique<Widget>();   // Widget* inside, zero overhead

// unique_ptr is move-only
auto q = std::move(p);  // q owns it now, p is null
// p = q;              // ERROR: copy is deleted

// Destruction is automatic
{
    auto w = std::make_unique<Widget>();
    w->do_something();
}   // Widget destroyed here — delete called automatically

// Factory functions: return unique_ptr
// Callers can later upgrade to shared_ptr if needed — forward-compatible
std::unique_ptr<Animal> make_animal(Kind k) {
    if (k == Kind::Dog) return std::make_unique<Dog>();
    if (k == Kind::Cat) return std::make_unique<Cat>();
    return nullptr;
}

std::shared_ptr<Animal> shared = make_animal(Kind::Dog);
// Converting unique_ptr → shared_ptr is trivial
// The reverse is impossible
// Custom deleters: for non-memory resources
// Stateless lambda deleter: same size as raw pointer (no penalty)
auto file_del = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_del)> f(fopen("data.bin", "r"), file_del);

// Function pointer deleter: grows unique_ptr by one pointer width
// (function pointer must be stored as state)
// → Prefer stateless lambdas for size-sensitive code

// Array form: use when you must manage a C-style heap array
// (prefer std::vector in almost all cases)
auto buf = std::make_unique<char[]>(1024);
buf[0] = 'H';  // operator[] is available for array form
// operator* and operator-> are NOT available for array form

unique_ptr at a glance

  • • Size: 1 pointer (default deleter) or 1–2 pointers (custom deleter)
  • • Copy: disabled — ownership is exclusive
  • • Move: O(1), transfers ownership, sets source to null
  • • Overhead vs raw pointer: zero (with default deleter)

shared_ptr — shared ownership

std::shared_ptr<T> implements reference-counted shared ownership. Multiple shared_ptrs can point to the same object. The object is destroyed when the last owner is destroyed or reset. The timing of destruction is deterministic — unlike garbage collection — but the ownership structure can be complex.

Internally, a shared_ptr is two pointers: one to the managed object and one to a control block. The control block lives on the heap and holds the reference count, the weak count, and the custom deleter. This means a shared_ptr is twice the size of a raw pointer. The reference count is updated atomically (thread-safe, but not free — one or two atomic operations per copy or destruction).

auto sp1 = std::make_shared<Widget>();  // refcount = 1
auto sp2 = sp1;                          // refcount = 2 (copy → atomic increment)
auto sp3 = std::move(sp1);              // refcount still 2 (move → no increment)
                                         // sp1 is now null

sp2.reset();   // refcount → 1 (atomic decrement)
// sp3 goes out of scope → refcount → 0 → Widget destroyed

// Copy vs move performance
void store(std::shared_ptr<Widget> sp);  // copy: +1 atomic increment
void store(std::shared_ptr<Widget>&& sp); // move: 0 atomic ops

// Custom deleter: NOT part of the type (unlike unique_ptr)
auto del = [](Widget* w) { cleanup(w); delete w; };
std::shared_ptr<Widget> sp(new Widget, del);
// Type is std::shared_ptr<Widget> regardless of deleter type
// → can mix shared_ptrs with different deleters in one container

Never create two shared_ptrs from the same raw pointer

Widget* pw = new Widget;
std::shared_ptr<Widget> sp1(pw);   // control block 1, refcount=1
std::shared_ptr<Widget> sp2(pw);   // control block 2, refcount=1 — WRONG!
// When sp1 dies: Widget deleted
// When sp2 dies: Widget deleted again → undefined behavior (double free)

Always construct from another shared_ptr, or use make_shared. If you must use new, pass it directly without a variable: shared_ptr<Widget>(new Widget).

// enable_shared_from_this: safely create shared_ptr to *this
// Without this, passing 'this' to a shared_ptr constructor is UB
class Widget : public std::enable_shared_from_this<Widget> {
public:
    static std::shared_ptr<Widget> create() {
        return std::shared_ptr<Widget>(new Widget);  // private ctor
    }

    void register_self(Registry& reg) {
        reg.add(shared_from_this());  // correct: reuses existing control block
        // reg.add(std::shared_ptr<Widget>(this));  // WRONG: new control block
    }
private:
    Widget() = default;
};
// Rule: if the class needs shared_from_this(), make its constructors private
// and provide a factory function — prevents pre-ownership use

shared_ptr at a glance

  • • Size: 2 pointers (ptr to object + ptr to control block)
  • • Copy: O(1) + 1 atomic increment
  • • Move: O(1), no atomic ops
  • • Destruction: O(1) + 1 atomic decrement (+ destructor if last owner)
  • • Control block: separate heap allocation (unless make_shared)

weak_ptr — non-owning observation

std::weak_ptr<T> is an augmentation of shared_ptr. It points to an object managed by a shared_ptr, but does not participate in ownership — it does not increment the reference count. This means the object can be destroyed while a weak_ptr still exists. A weak_ptr can detect that situation: it can tell whether what it points to has been destroyed.

You cannot dereference a weak_ptr directly — it has no operator* or operator->. To access the object, you must first lock it, which atomically checks whether the object still exists and, if so, creates a temporary shared_ptr to it. This prevents the race condition where the object is destroyed between the check and the access.

auto sp = std::make_shared<Widget>();  // refcount=1, weak_count=0

std::weak_ptr<Widget> wp(sp);          // refcount still 1, weak_count=1

sp = nullptr;                          // refcount→0: Widget destroyed
                                       // weak_count still 1, control block alive

// Check if expired (non-atomic — don't use for access)
if (wp.expired()) { /* gone */ }

// Lock: atomic check + access in one operation
if (auto live = wp.lock()) {           // live is shared_ptr<Widget> or null
    live->use_it();                    // safe: object is guaranteed alive here
}

// Alternative: throw if expired
try {
    std::shared_ptr<Widget> live(wp);  // throws std::bad_weak_ptr if expired
    live->use_it();
} catch (const std::bad_weak_ptr&) { /* handle */ }
// Use case 1: Caching — cache doesn't own the objects
std::unordered_map<ID, std::weak_ptr<Widget>> cache;

std::shared_ptr<Widget> get(ID id) {
    if (auto sp = cache[id].lock())
        return sp;         // still alive: return from cache
    auto sp = load(id);    // expired or not cached: reload
    cache[id] = sp;        // store weak reference
    return sp;
}

// Use case 2: Break reference cycles
// A and B should not hold shared_ptr to each other — cycle → neither freed
struct Node {
    std::shared_ptr<Node>  child;   // owns child
    std::weak_ptr<Node>    parent;  // observes parent, doesn't own
};

weak_ptr at a glance

  • • Size: same as shared_ptr (2 pointers)
  • • Does NOT affect the object's reference count (only the weak count)
  • • Control block stays alive until last weak_ptr dies (even after object is destroyed)
  • • Use cases: caching, observer patterns, breaking shared_ptr cycles

Always prefer make_unique and make_shared

The make functions (std::make_unique and std::make_shared) perfect-forward their arguments to the object's constructor and return a smart pointer. They are preferred over calling new directly for three reasons.

First, they eliminate type repetition. Writing make_unique<Widget>() names Widget once; unique_ptr<Widget>(new Widget) names it twice — a DRY violation that can silently diverge.

Second, they are exception-safe. Passing a raw new expression as a function argument alongside another expression that might throw can leak memory — the compiler is allowed to interleave the evaluation. make_shared eliminates this risk by combining allocation and pointer construction into one atomic step.

Third, make_shared is more efficient: it combines the object allocation and the control block allocation into a single heap allocation (vs. two separate allocations when using new).

// Prefer:
auto up = std::make_unique<Widget>(args...);   // C++14
auto sp = std::make_shared<Widget>(args...);   // C++11

// Avoid:
auto up = std::unique_ptr<Widget>(new Widget(args...));   // type repeated
auto sp = std::shared_ptr<Widget>(new Widget(args...));   // two allocations

// Exception-safety issue with new:
void process(std::shared_ptr<Widget> sp, int priority);
int computePriority();  // might throw

process(std::shared_ptr<Widget>(new Widget), computePriority());
// Compiler may evaluate in this order:
//  1. new Widget  (allocates memory)
//  2. computePriority() throws
//  3. shared_ptr constructor never runs → Widget leaks!

process(std::make_shared<Widget>(), computePriority());
// make_shared is a single expression → no interleaving possible → safe

// make_shared: single heap allocation (object + control block fused)
// Widget size: 48 bytes, control block: ~24 bytes
// new Widget:   two allocs → 48 + 24 = 72 bytes, two round trips to allocator
// make_shared:  one alloc  → 72 bytes, one round trip to allocator

When you must use new directly

  • • Custom deleter: make functions don't accept deleters — use new + immediate wrap
  • • Braced initialization: make_unique<vector<int>>(10, 20) calls the 2-arg constructor, not {10, 20}
  • • Very large shared objects with long-lived weak_ptrs: fused allocation delays freeing the object memory until all weak_ptrs are gone too

Choosing the right pointer

The choice is about ownership semantics, not performance. Always start with the weakest ownership that correctly models your problem.

SituationUseWhy
Single owner, not sharedunique_ptrZero overhead, expresses intent clearly
Factory function returnunique_ptrCallers can upgrade to shared_ptr if needed; reverse is impossible
Genuinely shared ownershipshared_ptrReference counting ensures object lives as long as any owner
Observer, cache, cycle-breakerweak_ptrDoesn't extend lifetime; can detect when object is gone
Non-owning alias (short lifetime)raw pointer or referenceNo ownership implied; safe when caller guarantees lifetime
Passing to a function that doesn't take ownershipconst T& or T*Avoid incurring refcount overhead; just observe

Key rules

Default to unique_ptr; reach for shared_ptr only when you need shared ownership

Shared ownership adds complexity and cost. Start exclusive, upgrade if necessary.

Prefer make_unique and make_shared over new

Type deduplication, exception safety, and (for make_shared) a single allocation.

Never construct two shared_ptrs from the same raw pointer

Creates two control blocks, two reference counts, and a double-free on destruction.

Inherit enable_shared_from_this when you need shared_ptr to this

The only safe way to get a shared_ptr from inside a member function.

Pass by const reference or raw pointer to functions that don't take ownership

Don't pay shared_ptr copy overhead (atomic increment) when you're just observing.

Use weak_ptr to break cycles and in observer/cache patterns

A cycle of shared_ptrs is a memory leak. weak_ptr breaks the cycle without dangling.

Sign in to track progress