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++11RAII 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.
#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.
// 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.
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 allocshared_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.
// 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.
std::shared_ptr<int[]> v{new int[8]}; // C++17 — calls delete[] correctly
auto buf = std::make_shared<int[]>(8); // C++20Aliasing 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.
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; // safeThread 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.
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
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 destroyedObserver pattern
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.
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 aliveRules: 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) andmake_shared(C++11) over wrapping a rawnewexpression — they are exception-safe, often more efficient, and keep allocation details local. - Return
unique_ptrfrom factories; let callers promote toshared_ptrif needed. - Accept smart pointers as function parameters only when the function participates in ownership management. For non-owning access pass
const T&,T&, orT*. - Prefer
const shared_ptr<T>&overshared_ptr<T>for read-only parameters that do not store the pointer — it avoids two atomic operations per call. - Use
weak_ptrfor 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.
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_ptr | shared_ptr | weak_ptr | T* / T& | |
|---|---|---|---|---|
| Ownership | Sole | Shared | None | None |
| Copyable | ❌ | ✅ | ✅ | ✅ |
| Size | 1 ptr (default deleter) | 2 ptrs | 2 ptrs | 1 ptr |
| Atomic ops | Zero | Copy / destroy | lock() | Zero |
| Null check | if (p) | if (p) | w.lock() | if (p) |
| Use for | Factory return, members | Shared graphs, caches | Cycle breaking, observers | Non-owning parameters |
See Also
- RAII — the ownership principle that smart pointers implement
- Move Semantics — why
unique_ptris move-only and how ownership transfer works std::optional— nullable value ownership without heap allocation- Custom Allocators — controlling where
shared_ptrplaces its control block