shared_ptr and weak_ptr
When a single owner is not enough — when multiple independent parts of a program legitimately need access to the same object and no single one of them outlives the others — std::shared_ptr provides reference-counted shared ownership. It keeps a count of how many shared_ptrs point to the object and deletes it when the last one is destroyed. std::weak_ptr is the companion: a non-owning observer that can check whether the object still exists without contributing to the count, used primarily to break reference cycles.
shared_ptr — reference-counted ownership
Every shared_ptr points to two things: the managed object and a separately heap-allocated control block that holds the reference count (and a weak count, and the deleter). When you copy a shared_ptr, the count increments. When one is destroyed or reset, the count decrements. When the count reaches zero, the managed object is deleted. Always create with std::make_shared — it allocates the object and the control block in a single allocation, saving an allocation and improving cache locality.
#include <memory>
auto sp1 = std::make_shared<int>(42);
std::cout << sp1.use_count(); // 1
{
auto sp2 = sp1; // copy — count becomes 2
std::cout << sp1.use_count(); // 2
// sp2 goes out of scope — count drops to 1
}
std::cout << sp1.use_count(); // 1
// sp1 goes out of scope — count drops to 0 — int deleted
// Member access is the same as unique_ptr:
auto w = std::make_shared<std::string>("hello");
std::cout << w->size(); // 5
std::cout << *w; // helloCopying vs moving shared_ptr
Copying a shared_ptr increments the reference count (an atomic operation — thread-safe but not free). Moving a shared_ptr transfers ownership without touching the count — the source becomes null. Prefer moving when you are passing ownership and no longer need the source.
auto a = std::make_shared<Widget>(); // count = 1 auto b = a; // copy: count = 2 (atomic increment) auto c = std::move(a); // move: count stays 2; a is now null std::cout << a.get(); // nullptr std::cout << b.use_count(); // 2 (b and c share ownership) // When to use each: // Pass by value (copy) when caller keeps using the shared_ptr after the call // Pass by std::move when transferring "my share" of ownership // Pass by const& when function only reads through the pointer — no count change
weak_ptr — observe without owning
A weak_ptr holds a non-owning reference to an object managed by a shared_ptr. It does not increment the reference count, so the object can be deleted even while weak_ptrs exist. To access the object you must lock the weak_ptr — this atomically checks whether the object still exists and, if so, returns a shared_ptr that temporarily extends its lifetime for the duration of that operation.
auto shared = std::make_shared<int>(99);
std::weak_ptr<int> weak = shared;
std::cout << weak.use_count(); // 1 — weak doesn't count
std::cout << weak.expired(); // false — object exists
// Lock to safely access:
if (auto locked = weak.lock()) {
std::cout << *locked; // 99 — safe to use
}
// Simulate the object being destroyed:
shared.reset(); // shared_ptr released, count = 0, object deleted
std::cout << weak.expired(); // true — object is gone
auto locked2 = weak.lock();
std::cout << (locked2 == nullptr); // true — lock returns null if expiredBreaking reference cycles
The classic pitfall of shared_ptr is the reference cycle: A holds a shared_ptr to B, and B holds a shared_ptr to A. Both reference counts stay at 1 even when there are no external owners — neither object is ever deleted. The fix is to make one direction of the cycle a weak_ptr: it observes without owning, so the cycle no longer keeps the objects alive.
// CYCLE — memory leak:
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // ← back-pointer also shared_ptr = cycle!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // cycle: a keeps b alive, b keeps a alive
// a and b go out of scope — neither is deleted!
// FIX — use weak_ptr for the back-pointer:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // ← observer, not owner
};
// Now when a goes out of scope, count drops to 0, a is deleted.
// b's weak_ptr to a becomes expired — safe.Cost and when to prefer unique_ptr
shared_ptr is more expensive than unique_ptr: it is larger (two pointers: object + control block), the control block is a separate heap allocation (unless make_shared is used), and every copy and destruction involves an atomic reference count operation. Prefer unique_ptr whenever a single owner suffices — use shared_ptr only when multiple independent owners genuinely need to outlive each other.
Key rules to remember
Use make_shared, not shared_ptr<T>(new T) — one allocation, better cache locality
make_shared allocates the object and the control block together. shared_ptr<T>(new T) makes two separate allocations.
Copying increments the atomic reference count — it has a cost
In hot loops, passing shared_ptr by const& (not copying) avoids unnecessary atomic operations. Only copy when you need to extend lifetime.
Always lock a weak_ptr before using it — the object may have been deleted
weak.lock() returns a shared_ptr that is null if the object is gone. Never access a weak_ptr's object directly.
Break shared_ptr cycles with weak_ptr on the back-edge
If A owns B and B needs a reference to A, make B's reference a weak_ptr. Otherwise both objects leak because the reference count never reaches zero.