Skip to content
C++
C++11Intermediate

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;          // hello

Copying 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 expired

Breaking 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.

Use unique_ptrOne clear owner — function, class member, container. Zero overhead versus raw pointer.
Use shared_ptrMultiple independent owners with no clear lifetime order. Caches, event listeners, graph nodes.
Use weak_ptrObserver that shouldn't extend lifetime. Back-pointers in trees/graphs, caches, avoiding cycles.

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.

Sign in to track progress