Skip to content
C++
Library
since C++11
Basic

Smart Pointers

unique_ptr, shared_ptr, and weak_ptr — ownership semantics, control block internals, custom deleters, cycle breaking, and enable_shared_from_this.

Smart Pointerssince C++11

RAII wrapper types in <memory> that bind a heap-allocated object's lifetime to an owning scope, enforcing ownership semantics statically and eliminating manual new/delete.

Overview

C++11 standardised three smart pointer types: unique_ptr (sole ownership, zero overhead), shared_ptr (shared, reference-counted ownership), and weak_ptr (non-owning observer). The predecessor auto_ptr (C++98) was deprecated in C++11 and removed in C++17 — never use it.

Default to unique_ptr. Reach for shared_ptr only when multiple, independent owners genuinely share responsibility for an object's lifetime. Use weak_ptr to break ownership cycles or observe a shared_ptr-managed object without extending its lifetime.


unique_ptr

unique_ptr<T> is a zero-overhead abstraction: with its default deleter it is the same size as a raw pointer and the destructor call is inlined. The type is move-only — transfer of ownership is explicit.

cpp
#include <memory>

// make_unique — introduced in C++14 (not C++11!)
auto p   = std::make_unique<Widget>(42, "hello");  // C++14
auto arr = std::make_unique<int[]>(256);            // C++14, array form

// Ownership transfer is explicit; copy is a compile error
auto p2 = std::move(p);    // p is now null
// auto p3 = p2;           // error: unique_ptr is not copyable

// release() — relinquish management; caller must delete
int* raw = p2.release();
delete raw;

// reset() — destroy current object, optionally take a new one
p2.reset();                    // destroy, p2 = null
p2.reset(new Widget(99));      // replace (avoid; prefer make_unique)

Custom deleters

A stateless functor or function pointer deleter costs nothing extra (EBO eliminates the member). A stateful lambda or capturing closure adds one pointer of storage.

cpp
// Stateless functor — sizeof == sizeof(void*)
struct FClose { void operator()(FILE* f) const { if (f) std::fclose(f); } };
std::unique_ptr<FILE, FClose> file{std::fopen("data.bin", "rb")};

// Aligned allocation — stateless lambda (C++14 generic lambda compiles to functor)
struct FreeAligned {
    void operator()(std::byte* p) const noexcept { std::free(p); }
};
std::unique_ptr<std::byte[], FreeAligned> buf{
    static_cast<std::byte*>(std::aligned_alloc(64, 4096))
};

Factory functions

unique_ptr is the canonical factory return type — sole ownership is documented at the call site, and it implicitly converts to shared_ptr when callers need shared semantics.

cpp
std::unique_ptr<Shape> make_shape(ShapeKind k) {
    switch (k) {
        case ShapeKind::Circle: return std::make_unique<Circle>();
        case ShapeKind::Rect:   return std::make_unique<Rect>();
        default:                return nullptr;
    }
}

auto s = make_shape(ShapeKind::Circle);          // unique ownership
std::shared_ptr<Shape> sp = make_shape(ShapeKind::Rect); // implicit promotion, no extra alloc

shared_ptr

shared_ptr<T> occupies two pointers: one to the managed object and one to a heap-allocated control block that holds the strong reference count, the weak reference count, the deleter, and (when not using make_shared) the allocator. Both counts are updated with atomic operations — not free on any architecture.

cpp
// Preferred: fused single allocation — object + control block contiguous
auto s = std::make_shared<Widget>();        // C++11

// Two-allocation form — avoid unless you need a custom deleter
std::shared_ptr<Widget> s2{new Widget()};   // C++11

auto s3 = s;              // copy: atomic strong-count increment
auto s4 = std::move(s);   // move: count unchanged, s becomes null

s4.use_count();  // 2 (s3 and s4)

make_shared trade-off. The fused allocation is cache-friendly and avoids a second heap call, but the raw memory cannot be reclaimed until both strong and weak counts reach zero. If large objects accumulate long-lived weak_ptr observers, the two-allocation form lets the object memory be freed independently of the control block.

C++17: array support

shared_ptr<T[]> gained proper array support in C++17. make_shared<T[]> accepting a size was added in C++20.

cpp
std::shared_ptr<int[]> v{new int[8]};   // C++17 — calls delete[] correctly
auto buf = std::make_shared<int[]>(8);  // C++20

Aliasing constructor

The aliasing constructor creates a shared_ptr that shares the control block of an existing shared_ptr but points to a different (sub-)object. The underlying allocation lives until all aliased pointers expire.

cpp
struct Packet { uint32_t magic; std::array<std::byte, 256> payload; };

auto pkt = std::make_shared<Packet>();
std::shared_ptr<uint32_t> magic(pkt, &pkt->magic);  // aliases into Packet
pkt.reset();               // Packet still alive — magic holds the ref
*magic = 0xDEADBEEF;       // safe

Thread safety

Atomic reference counting makes concurrent copies and destructions of shared_ptr instances safe across threads. The pointed-to object is not protected — concurrent reads/writes through different shared_ptr copies require external synchronisation. C++20 adds std::atomic<std::shared_ptr<T>> as a proper type, replacing the deprecated std::atomic_load/std::atomic_store free functions.


weak_ptr

weak_ptr<T> holds a non-owning reference backed by the control block's weak count. Atomically promoting a weak_ptr via lock() either returns a valid shared_ptr (object is alive) or a null one (object was destroyed). Never dereference through expired() — it is racy in multithreaded contexts.

cpp
auto s = std::make_shared<int>(42);
std::weak_ptr<int> w = s;

if (auto locked = w.lock()) {   // atomic check-and-promote
    *locked += 1;               // object guaranteed alive for this scope
}

Breaking ownership cycles

cpp
struct Node {
    int value;
    std::shared_ptr<Node> next;  // strong — owns the next node
    std::weak_ptr<Node>   prev;  // weak  — back-reference, no ownership
};

auto a = std::make_shared<Node>(1);
auto b = std::make_shared<Node>(2);
a->next = b;
b->prev = a;  // weak: does not extend a's lifetime
// When a and b go out of scope both are cleanly destroyed

Observer pattern

cpp
class EventBus {
    std::vector<std::weak_ptr<Listener>> listeners_;
public:
    void subscribe(std::shared_ptr<Listener> l) {
        listeners_.push_back(l);  // weak — bus does not extend lifetime
    }
    void broadcast(const Event& e) {
        std::erase_if(listeners_, [](const auto& w) { return w.expired(); });
        for (auto& w : listeners_)
            if (auto l = w.lock()) l->on_event(e);
    }
};

enable_shared_from_this

When a shared_ptr-managed object needs to produce a shared_ptr to itself (e.g., to extend lifetime across an async callback), inherit from std::enable_shared_from_this<T>. It internally stores a weak_ptr<T> that is seeded when the first shared_ptr to the object is created.

cpp
class Connection : public std::enable_shared_from_this<Connection> {
public:
    void start() {
        // shared_from_this() is safe here — we are already managed
        auto self = shared_from_this();
        async_read(socket_, buffer_, [self](std::error_code ec, std::size_t n) {
            if (!ec) self->handle_read(n);
        });
    }
private:
    asio::ip::tcp::socket socket_;
    std::array<std::byte, 4096> buffer_;
    void handle_read(std::size_t n);
};

// Must be created via shared_ptr
auto conn = std::make_shared<Connection>(std::move(socket));
conn->start();  // conn may go out of scope; lambda's 'self' keeps it alive

Rules: Never call shared_from_this() from a constructor — no control block exists yet; it throws std::bad_weak_ptr (guaranteed since C++17; undefined behaviour before). The object must be heap-allocated and managed before the call.


Best Practices

  • Prefer make_unique (C++14) and make_shared (C++11) over wrapping a raw new expression — they are exception-safe, often more efficient, and keep allocation details local.
  • Return unique_ptr from factories; let callers promote to shared_ptr if needed.
  • Accept smart pointers as function parameters only when the function participates in ownership management. For non-owning access pass const T&, T&, or T*.
  • Prefer const shared_ptr<T>& over shared_ptr<T> for read-only parameters that do not store the pointer — it avoids two atomic operations per call.
  • Use weak_ptr for any back-reference in a graph structure, observer registration, or cache entry whose validity the observer must query.

Common Pitfalls

Double-free from aliased raw pointer. Constructing two independent shared_ptrs from the same raw pointer creates two control blocks; both destructors attempt to delete the same address.

cpp
Widget* raw = new Widget;
std::shared_ptr<Widget> a{raw};
std::shared_ptr<Widget> b{raw};  // UNDEFINED BEHAVIOUR — two control blocks
// Fix: copy from a, or use make_shared
auto a = std::make_shared<Widget>();
auto b = a;

shared_from_this() before managed. Calling it in a constructor or on a stack object throws std::bad_weak_ptr (C++17+). Before C++17 the behaviour is undefined.

make_shared pins memory for weak observers. For objects with large inline storage and long-lived weak_ptrs, the object memory is not freed until the last weak_ptr expires. Profile before assuming make_shared is always optimal.

expired() is racy. Between expired() returning false and the subsequent dereference, another thread may destroy the last shared_ptr. Always use lock() and test the returned pointer.


Ownership Decision Guide

unique_ptrshared_ptrweak_ptrT* / T&
OwnershipSoleSharedNoneNone
Copyable
Size1 ptr (default deleter)2 ptrs2 ptrs1 ptr
Atomic opsZeroCopy / destroylock()Zero
Null checkif (p)if (p)w.lock()if (p)
Use forFactory return, membersShared graphs, cachesCycle breaking, observersNon-owning parameters

See Also

  • RAII — the ownership principle that smart pointers implement
  • Move Semantics — why unique_ptr is move-only and how ownership transfer works
  • std::optional — nullable value ownership without heap allocation
  • Custom Allocators — controlling where shared_ptr places its control block