std::shared_ptr
A reference-counted smart pointer that enables shared ownership of a dynamically allocated object, deleting it when the last owner is destroyed.
std::shared_ptrsince C++11A smart pointer that retains shared ownership of an object through a reference count, destroying the managed object when the last shared_ptr owning it is destroyed or reset.
Overview
std::shared_ptr<T> implements shared-ownership semantics via reference counting. Every shared_ptr that owns an object shares a control block: a heap-allocated structure tracking the strong reference count, a weak reference count (for std::weak_ptr), and an embedded deleter and allocator. When the strong count falls to zero, the managed object is destroyed. The control block itself persists until the weak count also reaches zero.
Two construction strategies exist: constructing from a raw new expression, or using std::make_shared. Prefer make_shared in nearly all cases β it performs a single allocation for both the object and the control block, improving cache locality and eliminating an allocation. Direct construction from new requires two separate heap allocations.
// Two allocations: one for Widget, one for control block
std::shared_ptr<Widget> sp1(new Widget(42));
// Single allocation: object and control block are colocated (C++11)
auto sp2 = std::make_shared<Widget>(42);The reference count operations are atomic (typically using std::atomic internally), so copying and destroying shared_ptr instances from different threads is safe. The managed object itself is not protected β concurrent access to the pointed-to data requires external synchronization.
Syntax
#include <memory>
// Construction
std::shared_ptr<T> p1; // null, default-constructed
std::shared_ptr<T> p2(new T(args...)); // owns raw pointer
auto p3 = std::make_shared<T>(args...); // preferred (C++11)
auto p4 = std::make_shared<T[]>(n); // array support (C++20)
// Copying and moving
std::shared_ptr<T> p5 = p3; // increments refcount
std::shared_ptr<T> p6 = std::move(p3); // transfers ownership, p3 becomes null
// Observers
p5.use_count(); // strong reference count (approximate in multithreaded code)
p5.get(); // raw pointer, non-owning
p5.unique(); // true iff use_count() == 1 (deprecated in C++17, removed C++20)
static_cast<bool>(p5); // false iff null
// Modifiers
p5.reset(); // releases ownership
p5.reset(new T(args...)); // releases old, takes new
// Casts (C++11)
std::static_pointer_cast<Derived>(base_sp);
std::dynamic_pointer_cast<Derived>(base_sp); // returns null if cast fails
std::const_pointer_cast<T>(const_sp);
std::reinterpret_pointer_cast<U>(sp); // C++17Examples
Basic shared ownership
#include <memory>
#include <vector>
#include <iostream>
struct Node {
int value;
explicit Node(int v) : value(v) {
std::cout << "Node(" << v << ") constructed\n";
}
~Node() {
std::cout << "Node(" << value << ") destroyed\n";
}
};
void inspect(std::shared_ptr<Node> sp) { // copy: refcount +1
std::cout << "use_count inside: " << sp.use_count() << '\n';
}
int main() {
auto sp = std::make_shared<Node>(10); // refcount = 1
inspect(sp); // refcount = 2 inside, back to 1
{
auto sp2 = sp; // refcount = 2
std::cout << sp.use_count() << '\n'; // 2
} // sp2 destroyed, refcount = 1
// sp destroyed here -> Node(10) destroyed
}Custom deleters
shared_ptr accepts a callable deleter at construction time, stored type-erased in the control block. The deleter is part of the runtime state, not the type β unlike unique_ptr<T, Deleter>.
// File handle managed with custom deleter
auto file_deleter = [](FILE* f) {
if (f) std::fclose(f);
};
std::shared_ptr<FILE> file(std::fopen("data.bin", "rb"), file_deleter);
if (!file) throw std::runtime_error("failed to open");
// Use file.get() for C API calls
std::fread(buffer, 1, size, file.get());
// File closed automatically when last shared_ptr is destroyedAliasing constructor
The aliasing constructor (C++11) lets a shared_ptr<U> share ownership with a shared_ptr<T> while pointing to a different object β typically a member. The managed object (and its lifetime) is governed by the original shared_ptr<T>, but the aliasing pointer stores a different raw address.
struct Config {
std::string host;
uint16_t port;
};
auto cfg = std::make_shared<Config>("localhost", 8080);
// shared_ptr to member β keeps cfg alive, points at cfg->port
std::shared_ptr<uint16_t> port_ptr(cfg, &cfg->port);
cfg.reset(); // cfg's refcount drops to 1 (port_ptr holds it)
std::cout << *port_ptr << '\n'; // safe: Config still aliveThis pattern is used in std::shared_ptr implementations of observer registration and in certain container nodes.
enable_shared_from_this
When a member function needs to produce a shared_ptr to *this, inheriting from std::enable_shared_from_this<T> is the correct approach. Never construct a shared_ptr directly from this β that creates a second, independent control block, leading to a double-free.
#include <memory>
class Connection : public std::enable_shared_from_this<Connection> {
public:
static std::shared_ptr<Connection> create() {
return std::shared_ptr<Connection>(new Connection);
}
std::shared_ptr<Connection> self() {
return shared_from_this(); // correct: shares the existing control block
}
private:
Connection() = default;
};
// WRONG: creates two independent control blocks -> double-free
// Connection c;
// auto sp1 = std::make_shared<Connection>(c); // no
// auto sp2 = std::shared_ptr<Connection>(&c); // noThe base class stores a weak_ptr that is set when the first shared_ptr to the object is constructed β which is why construction must go through a factory or make_shared.
Best Practices
Use make_shared by default. A single allocation for object and control block is faster, more cache-friendly, and exception-safe in the general case.
Pass shared_ptr by value only when sharing ownership. If a function merely uses the object without extending its lifetime, accept const T& or T* instead. Copying a shared_ptr is not free β atomic increment/decrement has measurable cost on multi-core systems under high contention.
// Prefers non-owning access; caller decides lifetime
void process(const Widget& w);
void process(Widget* w); // nullable variant
// Use this only when the callee genuinely needs to share ownership
void register_handler(std::shared_ptr<Handler> h);Break ownership cycles with std::weak_ptr. A weak_ptr observes a shared_ptr-managed object without participating in its reference count. Promote it with lock(), which returns a shared_ptr or null if the object was already destroyed.
struct Parent {
std::vector<std::shared_ptr<Child>> children;
};
struct Child {
std::weak_ptr<Parent> parent; // NOT shared_ptr β avoids cycle
void notify() {
if (auto p = parent.lock()) {
// p is a shared_ptr, Parent is still alive
}
}
};Prefer unique_ptr as the default. shared_ptr carries overhead β two pointer-sized members plus a heap-allocated control block. Reach for it only when lifetime genuinely cannot be statically scoped or tied to a single owner.
Common Pitfalls
Constructing from the same raw pointer twice creates two control blocks with independent counts. Both will attempt to delete the object.
Widget* raw = new Widget;
std::shared_ptr<Widget> sp1(raw);
std::shared_ptr<Widget> sp2(raw); // undefined behavior: two control blocksuse_count() is unreliable for synchronization decisions. In multithreaded code, the count can change between observation and action. Use unique() (removed in C++20) or redesign the ownership model instead of polling the count.
make_shared and custom deleters don't mix. make_shared allocates object and control block together; a custom deleter requires separate allocation. Use the constructor form when a non-default deleter is needed.
The managed object outlives the control block if weak_ptr references exist. When using make_shared, the object's storage is part of the same allocation as the control block. The memory cannot be freed until both the strong and weak counts reach zero β even after the object itself is destroyed. For large objects with long-lived weak_ptr observers, two-argument construction (shared_ptr<T>(new T(...))) avoids this.
Avoid raw this captures in lambdas stored in shared_ptr-owned objects. The lambda may outlive the object if stored elsewhere.
// Dangerous: raw this captured in async callback
void Widget::schedule() {
enqueue([this] { this->update(); }); // Widget may be gone by the time callback fires
}
// Safe: capture by shared_ptr via shared_from_this()
void Widget::schedule() {
auto self = shared_from_this();
enqueue([self] { self->update(); });
}make_shared vs. Direct Construction: Memory Layout
The choice between make_shared and shared_ptr<T>(new T(...)) affects how memory is allocated and how long it lives.
Direct construction (shared_ptr<T>(new T(...))) performs two heap allocations: one for the T object and a separate one for the control block. The control block holds the strong and weak reference counts, the deleter, and the allocator. These two allocations are freed independently.
make_shared performs a single heap allocation that holds both the control block and the T object packed together (a "combo block"). This has two consequences:
- Better cache locality: the control block is adjacent to the managed object, so accessing either after a
shared_ptrcopy is more likely to hit the same cache line. - The combo block cannot be freed until both the strong and weak counts reach zero. When the last
shared_ptris destroyed,T's destructor runs, but the memory stays allocated until everyweak_ptris also gone. With direct construction, only the control block lingers; theTmemory is freed as soon as the lastshared_ptrgoes.
// Two allocations β object memory freed when last shared_ptr is gone
auto p1 = std::shared_ptr<Widget>(new Widget(42));
// Single allocation β object and control block freed together
// (memory lingers if weak_ptr observers outlive all shared_ptr owners)
auto p2 = std::make_shared<Widget>(42);When to prefer direct construction:
- You need a custom deleter (e.g.,
fclose, a pool free).make_sharedhas no deleter parameter. sizeof(T)is large and you expect long-livedweak_ptrobservers (caches, registries). In that case, two-argument construction frees theTstorage as soon as ownership ends.
In all other cases, prefer make_shared.
std::atomic<std::shared_ptr<T>> (C++20, P0718R2)
Before C++20, the standard provided free functions (std::atomic_load, std::atomic_store, std::atomic_exchange, std::atomic_compare_exchange_weak/strong) for atomic access to a shared_ptr. These were deprecated in C++20 (and removed in C++26). The replacement is a proper partial template specialization:
#include <memory>
#include <atomic>
// C++20: atomic specialization for shared_ptr (P0718R2)
std::atomic<std::shared_ptr<Widget>> g_widget;
// Reader thread
void reader() {
auto local = g_widget.load(std::memory_order_acquire);
if (local) local->use();
}
// Writer thread β atomically replaces the shared pointer
void writer(std::shared_ptr<Widget> next) {
g_widget.store(std::move(next), std::memory_order_release);
}
// Lock-free compare-and-swap on the pointer
void update_if_unchanged(std::shared_ptr<Widget> expected,
std::shared_ptr<Widget> desired) {
// compare_exchange_strong checks: same stored T* AND same ownership
g_widget.compare_exchange_strong(expected, std::move(desired));
}Key properties from cppreference (C++20):
load(),store(),exchange(),compare_exchange_weak/strong()are all provided.use_countincrements are guaranteed to be part of the atomic operation. Decrements are sequenced after it.is_lock_free()returns whether the implementation can avoid internal locking. In practice, most current implementations (libstdc++, MSVC STL) use a lock bit embedded in the low bits of the control block pointer β so the specialization is usually not lock-free on commodity hardware (clang libc++ had not shipped the specialization as of late 2024).- The standard also adds
wait(),notify_one(), andnotify_all()for atomic blocking. - Feature-test macro:
__cpp_lib_atomic_shared_ptr >= 201711L.
Why it replaces the old free functions: The old free functions operated on a plain shared_ptr<T> object and required all accesses to that object β including non-atomic ones β to go through the atomic API. There was no way for the compiler to enforce this. The new std::atomic<shared_ptr<T>> wraps the pointer inside an atomic type, making misuse a compile error and the intent explicit.
Note on is_always_lock_free: Because a shared_ptr is two words wide (stored pointer + control block pointer), a true lock-free CAS would require a double-width atomic instruction (DWCAS). Whether this is available and used is implementation-defined. If hard lock-freedom matters, check std::atomic<std::shared_ptr<T>>::is_always_lock_free at compile time.
// Check at compile time if your platform supports lock-free atomic shared_ptr
static_assert(std::atomic<std::shared_ptr<Widget>>::is_always_lock_free,
"atomic<shared_ptr> is not lock-free on this platform");enable_shared_from_this and weak_from_this (C++17)
The enable_shared_from_this base class was introduced in C++11. C++17 added weak_from_this(), which returns a weak_ptr<T> to *this without the risk of throwing std::bad_weak_ptr.
Motivation for weak_from_this: shared_from_this() throws std::bad_weak_ptr if called on an object not yet managed by any shared_ptr (e.g., during construction). weak_from_this() returns an expired weak_ptr in that case instead of throwing, making it safe to call from constructors or before the first shared_ptr is created.
#include <memory>
#include <iostream>
class Session : public std::enable_shared_from_this<Session> {
public:
// C++17: returns weak_ptr without risk of throwing
std::weak_ptr<Session> weak_self() {
return weak_from_this();
}
// C++11: throws std::bad_weak_ptr if not managed by a shared_ptr
std::shared_ptr<Session> shared_self() {
return shared_from_this();
}
void schedule_timeout(int ms) {
// Capture weak_ptr so the callback does not extend the lifetime
auto weak = weak_from_this();
post_after(ms, [weak] {
if (auto self = weak.lock()) {
self->on_timeout();
}
// If Session was already destroyed, weak.lock() returns null
});
}
private:
void on_timeout() {
std::cout << "Session timed out\n";
}
void post_after(int /*ms*/, auto /*fn*/) {
/* timer implementation */
}
};
int main() {
auto s = std::make_shared<Session>();
// weak_from_this is safe in constructors too (returns expired weak_ptr then)
auto w = s->weak_self();
std::cout << "expired? " << w.expired() << '\n'; // 0 (not expired)
}How the internal weak_ptr is populated: std::enable_shared_from_this<T> stores a mutable std::weak_ptr<T> data member (weak_this). When a shared_ptr<T> is constructed from a T* and the type has an accessible enable_shared_from_this base, the shared_ptr constructor detects it (via type traits) and initialises weak_this from the new control block. This is why the object must be managed through a shared_ptr before calling shared_from_this().
C++17 change in bad_weak_ptr behaviour: Before C++17, calling shared_from_this() on an unmanaged object was undefined behaviour. C++17 guaranteed the throw, making it safe to catch and handle.
// C++17: calling shared_from_this on a stack object throws, not UB
class Foo : public std::enable_shared_from_this<Foo> {};
void demo() {
Foo f;
try {
auto sp = f.shared_from_this(); // throws std::bad_weak_ptr (C++17+)
} catch (const std::bad_weak_ptr&) {
// correctly caught
}
// weak_from_this returns expired weak_ptr β no throw
auto wp = f.weak_from_this();
assert(wp.expired());
}Aliasing Constructor: Advanced Patterns
The aliasing constructor decouples the stored pointer from the ownership pointer. A shared_ptr<U> built with the aliasing form std::shared_ptr<U>(owner, ptr) shares the control block of owner but stores ptr as its raw address. The result is a shared_ptr<U> whose get() returns ptr and whose lifetime keeps the object managed by owner alive β even if U and T are unrelated types.
This unlocks patterns beyond simple member-pointer aliasing. A common use is slicing into a shared array without copying, or handing out a typed view into a type-erased buffer.
// C++11
#include <memory>
#include <array>
struct Packet {
std::array<std::byte, 1500> payload;
uint16_t length;
};
// Share ownership of the whole Packet but expose only the payload span
auto pkt = std::make_shared<Packet>();
pkt->length = 64;
std::shared_ptr<std::byte> payload_view(pkt, pkt->payload.data());
// pkt can now be reset; payload_view keeps the Packet alive
pkt.reset();
// Safe: Packet still alive because payload_view shares its control block
auto* raw = payload_view.get(); // points into Packet::payloadAliased pointers preserve const-correctness across the type boundary. If owner is a shared_ptr<const Config>, an aliasing pointer to one of its members inherits const by the type you pick for U β the compiler does not enforce it automatically, so choose U = const MemberType.
// C++11
struct Config { std::string host; uint16_t port; };
auto cfg = std::make_shared<const Config>(Config{"localhost", 9090});
// Correctly propagate const to the aliasing pointer
std::shared_ptr<const uint16_t> port_alias(cfg, &cfg->port);
// *port_alias = 80; // compile error: const uint16_tThe aliasing constructor also works with arrays. In C++20, std::make_shared<T[]>(n) allocates a shared array; you can alias individual elements by index.
// C++20
auto arr = std::make_shared<int[]>(8);
arr[3] = 42;
// Alias element 3 β lifetime tied to the whole array
std::shared_ptr<int> elem(arr, arr.get() + 3);
arr.reset();
std::cout << *elem << '\n'; // 42, array still alivestd::atomic<std::shared_ptr<T>> β RCU Read Pattern (C++20)
Read-Copy-Update (RCU) is a lock-free publishing idiom that pairs naturally with std::atomic<std::shared_ptr<T>>. The core idea: readers atomically load the current pointer and work with their local copy; a writer builds a new object, then atomically swaps it in. Readers that already hold the old pointer see the old object until they release it; the object is destroyed only when the last shared_ptr drops.
This pattern appears in configuration hot-reload, read-heavy caches, and single-writer/many-reader service discovery tables.
// C++20
#include <memory>
#include <atomic>
#include <thread>
#include <vector>
struct Config {
std::string endpoint;
int timeout_ms;
};
// Shared, atomically updated config β no mutex required for readers
std::atomic<std::shared_ptr<Config>> g_config{
std::make_shared<Config>(Config{"api.example.com", 5000})
};
// Reader: O(1) acquire, no lock contention
void handle_request() {
// C++20: load is atomic; local copy pins the Config object
auto cfg = g_config.load(std::memory_order_acquire);
// Use cfg->endpoint, cfg->timeout_ms freely β cfg keeps object alive
} // cfg's destructor atomically decrements the ref count
// Writer: build new config, swap atomically
void reload_config(std::string new_endpoint, int new_timeout) {
auto next = std::make_shared<Config>(Config{
std::move(new_endpoint), new_timeout
});
g_config.store(std::move(next), std::memory_order_release);
// Old Config is destroyed when every in-flight reader releases its copy
}
// CAS-based conditional update: only publish if still on the expected version
bool update_timeout(std::shared_ptr<Config> expected, int new_timeout) {
auto next = std::make_shared<Config>(Config{
expected->endpoint, new_timeout
});
return g_config.compare_exchange_strong(
expected, std::move(next),
std::memory_order_acq_rel, std::memory_order_acquire);
}The old std::atomic_load / std::atomic_store free functions are deprecated in C++20 and removed in C++26. They required every access site to remember to call the atomic variant; accidental plain reads created data races with no compiler diagnostic. The std::atomic<shared_ptr<T>> wrapper makes the atomic access the only API available on the object.
Performance Overhead and Benchmark Guidance
shared_ptr is not free β understand the three cost centres before reaching for it. The overhead comes from: (1) a heap allocation for the control block, (2) atomic reference-count increments and decrements on every copy and destroy, and (3) a pointer indirection through the control block on every count-modifying operation.
Cost summary (rough order of magnitude on x86-64, 2020s hardware):
make_shared<T> β 1 allocation + construct T + initialise counts
shared_ptr<T>(new) β 2 allocations + construct T + initialise counts
copy shared_ptr β 1 atomic fetch_add (strong count)
destroy shared_ptr β 1 atomic fetch_sub; if result == 0: destroy T + 1 more atomic fetch_sub (weak count)
unique_ptr copy β (deleted; move is free: single pointer swap)Copying a shared_ptr under contention is expensive. An atomic increment on a cache line shared across cores triggers a cache-coherence round-trip (MESI invalidate/read-exclusive). Benchmarks on contended shared_ptr copies can show 10β50Γ regression vs. a raw pointer copy. Strategies to mitigate:
// C++11
// --- Bad: copies shared_ptr in every iteration, hammering the refcount ---
void process_batch(const std::vector<std::shared_ptr<Widget>>& items) {
for (auto& sp : items) {
auto local = sp; // atomic increment per element
local->process();
}
}
// --- Better: borrow by const-ref when ownership is not needed ---
void process_batch(const std::vector<std::shared_ptr<Widget>>& items) {
for (const auto& sp : items) {
sp->process(); // no refcount touch; raw ptr dereference only
}
}
// --- Best for hot paths: extract raw pointer once ---
void process_batch(std::shared_ptr<Widget> sp) {
Widget* raw = sp.get(); // no refcount touch in the loop
for (int i = 0; i < 1'000'000; ++i) {
raw->process();
}
}make_shared reduces allocation cost but not refcount cost. The single-allocation benefit is one-time at construction. Every copy/destroy still pays the atomic cost. For objects constructed once and passed around many times (e.g., immutable configuration), make_shared is a clear win. For short-lived temporaries copied frequently, consider whether unique_ptr or value semantics fit instead.
Profile before optimising. On lightly-loaded systems or in single-threaded code, atomic operations on uncontended cache lines are cheap (often 1β3 ns). The cost compounds under real-world thread count and shared-pointer aliasing breadth. Use perf stat, valgrind --tool=callgrind, or a micro-benchmark framework (e.g., Google Benchmark) targeting the specific copy/pass pattern in your code before switching ownership strategies.
See Also
std::unique_ptrβ exclusive ownership, zero overheadstd::weak_ptrβ non-owning observer forshared_ptr-managed objectsstd::make_sharedβ preferred factory forshared_ptrstd::enable_shared_from_thisβ safeshared_ptrfromthis- RAII idiom β the ownership model underlying smart pointers