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

Custom deleters

Custom deleters let unique_ptr and shared_ptr manage resources that are not freed with delete. The deleter type is part of unique_ptr's type signature but is type-erased inside shared_ptr's control block.

cpp
// unique_ptr — deleter is a template parameter; stateless = zero overhead  // C++11
struct MunmapDeleter {
    std::size_t length;
    void operator()(void* p) const noexcept { ::munmap(p, length); }
};
void* addr = ::mmap(nullptr, 4096, PROT_READ|PROT_WRITE,
                    MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
std::unique_ptr<void, MunmapDeleter> mapping{addr, MunmapDeleter{4096}};

// shared_ptr — deleter is type-erased; any callable works             // C++11
auto handle = std::shared_ptr<sqlite3>(
    raw_db_ptr,
    [](sqlite3* db) { sqlite3_close(db); }
);

For unique_ptr, prefer a stateless deleter (plain struct with operator()) to keep the pointer's sizeof equal to one raw pointer. A stateful deleter (capturing lambda, closure) adds its captured state to the object's size.

cpp
// Runtime-selected deleter stored in unique_ptr via std::function — costs extra  // C++11
using Cleanup = std::function<void(FILE*)>;
std::unique_ptr<FILE, Cleanup> f{
    std::fopen("log.txt", "w"),
    [](FILE* fp) { std::fclose(fp); std::puts("file closed"); }
};

enable_shared_from_this (extended patterns)

enable_shared_from_this<T> is the correct way for a shared_ptr-managed object to hand out additional shared_ptr instances that share its control block. Attempting to construct a new shared_ptr directly from this would create a second, independent control block and lead to a double-free.

cpp
// Async lifetime extension — classic Asio idiom                       // C++11
class Session : public std::enable_shared_from_this<Session> {
public:
    static std::shared_ptr<Session> create(asio::ip::tcp::socket s) {
        return std::shared_ptr<Session>(new Session(std::move(s)));
        // make_shared would work too; private ctor forces factory
    }
    void run() {
        auto self = shared_from_this();   // ref-count +1 for duration of lambda
        socket_.async_read_some(asio::buffer(buf_),
            [self](std::error_code ec, std::size_t n) {
                if (!ec) self->process(n);
            });
    }
private:
    explicit Session(asio::ip::tcp::socket s) : socket_(std::move(s)) {}
    asio::ip::tcp::socket socket_;
    std::array<char, 1024> buf_;
    void process(std::size_t n);
};

C++17 added std::enable_shared_from_this<T>::weak_from_this(), which returns the internal weak_ptr<T> directly — useful when you need a non-owning self-reference without incurring a ref-count bump.

cpp
// weak_from_this() — C++17
std::weak_ptr<Session> observer = shared_from_this()->weak_from_this();

Aliasing constructor (shared_ptr)

The aliasing constructor lets one shared_ptr manage the lifetime of an allocation while pointing to a sub-object or unrelated address within it. This is invaluable when handing out pointers to members of a shared struct without copying the struct or splitting ownership.

cpp
struct Config {                                                          // C++11
    std::string host;
    uint16_t    port;
    bool        tls;
};

auto cfg = std::make_shared<Config>("example.com", 443, true);

// Alias: control block is shared with cfg; pointed-to object is just 'port'
std::shared_ptr<uint16_t> port_ptr(cfg, &cfg->port);
cfg.reset();          // cfg released — but Config still alive via port_ptr
*port_ptr = 8443;     // perfectly safe

A common use is extracting an element from a shared container without copying the entire container.

cpp
auto vec = std::make_shared<std::vector<int>>(std::initializer_list<int>{1,2,3,4,5});
// Alias to element [2] — vec's backing storage stays alive                // C++11
std::shared_ptr<int> elem(vec, &(*vec)[2]);
vec.reset();   // vector still alive
std::cout << *elem;  // 3

weak_ptr and cache patterns

weak_ptr is the canonical building block for non-owning caches: entries are automatically invalidated when the last external owner drops the object. No explicit eviction logic is needed for lifetime-based expiry.

cpp
// Intrusive cache — returns existing object if alive, creates otherwise   // C++11
class TextureCache {
    std::unordered_map<std::string, std::weak_ptr<Texture>> map_;
    std::mutex mtx_;
public:
    std::shared_ptr<Texture> get(const std::string& path) {
        std::lock_guard lk(mtx_);                         // C++17 CTAD
        auto& w = map_[path];
        if (auto sp = w.lock()) return sp;                // still alive
        auto sp = std::make_shared<Texture>(path);
        w = sp;                                           // store weak ref
        return sp;
    }
};

Periodically purge expired entries to avoid map growth from dead keys.

cpp
void TextureCache::purge() {                                               // C++20
    std::erase_if(map_, [](const auto& kv) { return kv.second.expired(); });
}

unique_ptr with arrays

unique_ptr<T[]> provides a zero-overhead, RAII-managed heap array. It calls delete[] on destruction, and its subscript operator[] provides bounds-free element access. Prefer std::vector for dynamic sizes; use the array form when you need a fixed-size heap buffer without container overhead (e.g., I/O buffers, interop with C APIs).

cpp
// make_unique for arrays — C++14
auto buf = std::make_unique<std::byte[]>(4096);   // C++14
buf[0] = std::byte{0xFF};
// buf is delete[]'d automatically

// C API interop — take raw pointer for duration of call
::read(fd, buf.get(), 4096);

// Sized array form — known at compile time can use std::array instead
constexpr std::size_t kFrameSize = 1024;
auto frame = std::make_unique<float[]>(kFrameSize);  // C++14

Note: unique_ptr<T[]> deliberately omits pointer arithmetic (++, --) and the dereference operator * — use get() and operator[] only.

cpp
// Transfer to caller as raw pointer (C ABI)
float* release_frame(std::unique_ptr<float[]>& p) {
    return p.release();   // caller must delete[]
}

Performance comparison table

Understanding the cost model prevents premature pessimisation and guides the choice between ownership strategies.

Operationunique_ptrshared_ptrweak_ptr
sizeof (default deleter)1 × ptr2 × ptr2 × ptr
Construction (make_*)1 alloc1 alloc (fused)0 alloc (from shared_ptr)
Construction (raw ptr)0 extra alloc1 extra alloc (control block)—
CopyDeletedAtomic incrementAtomic increment
MovePointer swap, O(1)Pointer swap, O(1)Pointer swap, O(1)
Destruction (last owner)1 deleteAtomic decrement + delete when 0Atomic decrement; free ctrl block when weak=0
lock()N/AN/AAtomic CAS
Thread-safe for the pointer itselfYes (move is single-threaded)YesYes
Thread-safe for pointed-to objectUser responsibilityUser responsibilityUser responsibility

Atomic operations on modern x86 hardware are cheap but not free — a shared_ptr copy/destroy pair costs roughly 2–10 ns per operation. In hot paths (tight loops, high-frequency event dispatch) prefer passing by const shared_ptr<T>& or restructure to unique_ptr.


Common pitfall: shared_ptr cycles

Reference cycles prevent shared_ptr from ever reaching a zero strong count, causing memory leaks that are invisible to sanitisers and valgrind's standard checks. The fix is always to make at least one edge of the cycle a weak_ptr.

cpp
// LEAK — both nodes own each other; counts never reach zero             // C++11
struct Bad {
    std::shared_ptr<Bad> peer;
};
{
    auto a = std::make_shared<Bad>();
    auto b = std::make_shared<Bad>();
    a->peer = b;  // a's count = 2 (main + b->peer)
    b->peer = a;  // b's count = 2 (main + a->peer)
}   // main refs drop to 1 each — memory leaked!
cpp
// FIXED — back-edge is weak                                             // C++11
struct Good {
    std::shared_ptr<Good> next;
    std::weak_ptr<Good>   prev;   // non-owning back-reference
};
{
    auto a = std::make_shared<Good>();
    auto b = std::make_shared<Good>();
    a->next = b;
    b->prev = a;  // does not increment a's strong count
}   // a and b correctly destroyed in reverse order

For tree structures, the parent should own children via shared_ptr or unique_ptr; children should reference their parent via weak_ptr. For graphs with no clear ownership hierarchy, consider an arena/pool that owns all nodes and dispenses raw or non-owning pointers.


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