Skip to content
C++
Library
since C++11
Intermediate

std::weak_ptr

A non-owning smart pointer that observes a shared_ptr-managed object without extending its lifetime, enabling cycle-breaking and safe expiry detection.

std::weak_ptr<T>since C++11

A non-owning smart pointer that holds a weak reference to an object managed by std::shared_ptr, allowing the object's lifetime to be observed and its validity to be checked without participating in ownership.

Overview

std::weak_ptr<T> is the non-owning companion to std::shared_ptr<T>. Where shared_ptr participates in the reference count that governs object lifetime, weak_ptr does not. It borrows visibility into the managed object without incrementing the strong reference count.

Every shared_ptr and weak_ptr referring to the same object share a control block β€” a heap-allocated structure with two independent counters:

  • Strong count: the number of shared_ptr instances owning the object. When this reaches zero, the managed object is destroyed.
  • Weak count: the number of weak_ptr instances, plus one for as long as the strong count is non-zero. When both counts reach zero, the control block itself is deallocated.

This split is why weak_ptr can safely detect expiry at any time: the control block outlives the managed object, so a validity check is always safe even after the object's memory has been reclaimed.

Primary use cases

Breaking reference cycles. When shared_ptr objects form mutual ownership cycles, their reference counts never reach zero and memory leaks. The non-owning direction of any bidirectional relationship should use weak_ptr:

cpp
struct Node : std::enable_shared_from_this<Node> {  // C++11
    std::string                         label;
    std::weak_ptr<Node>                 parent;    // non-owning upward link
    std::vector<std::shared_ptr<Node>>  children;  // owning downward links

    void append(std::shared_ptr<Node> child) {
        child->parent = weak_from_this();          // weak_from_this() since C++17
        children.push_back(std::move(child));      // std::move β€” C++11
    }

    std::string parentLabel() const {
        if (auto p = parent.lock()) return p->label;
        return "(root)";
    }
};

Non-prolonging caches. Return a live object if one already exists, recreate it otherwise, without preventing destruction when no caller holds a strong reference:

cpp
std::weak_ptr<Config> g_config_cache;

std::shared_ptr<Config> get_config() {
    if (auto p = g_config_cache.lock()) return p;
    auto p = std::make_shared<Config>(load_from_disk());
    g_config_cache = p;
    return p;
}

Observer / publish-subscribe patterns. Subscribers should never prevent a subject from being destroyed. Store handles as weak_ptr; on notification, lock() each one and prune those that have expired.

Syntax

cpp
#include <memory>

std::shared_ptr<T> sp = std::make_shared<T>(/*args*/);   // C++11

// Construct from shared_ptr (implicit conversion allowed)
std::weak_ptr<T> wp  = sp;
std::weak_ptr<T> wp2(sp);

// Promote to shared_ptr atomically β€” returns empty if expired
if (auto locked = wp.lock()) {
    use(*locked);
}

// Expiry check β€” unreliable for access in concurrent code (see pitfalls)
bool dead = wp.expired();              // C++11

// Observers
long n = wp.use_count();               // number of owning shared_ptrs; C++11

// Release the weak reference
wp.reset();                            // C++11

// Swap two weak_ptrs
wp.swap(wp2);                          // C++11

Examples

Observer pattern with automatic subscriber cleanup

cpp
#include <memory>
#include <vector>
#include <functional>
#include <algorithm>

class Button {
public:
    using Callback = std::function<void()>;

    // Returns a token β€” caller holds it; dropping the token unsubscribes
    [[nodiscard]]
    std::shared_ptr<Callback> on_click(Callback cb) {
        auto token = std::make_shared<Callback>(std::move(cb));
        listeners_.push_back(token);   // stores weak_ptr β€” C++11
        return token;
    }

    void click() {
        // Remove expired listeners, then fire live ones
        std::erase_if(listeners_, [](const auto& wp) { return wp.expired(); }); // std::erase_if β€” C++20
        for (auto& wp : listeners_)
            if (auto sp = wp.lock()) (*sp)();
    }

private:
    std::vector<std::weak_ptr<Callback>> listeners_;
};

int main() {
    Button btn;

    auto t1 = btn.on_click([] { /* handler A */ });
    {
        auto t2 = btn.on_click([] { /* handler B */ });
        btn.click();   // A and B both fire
    }  // t2 destroyed β€” handler B expires

    btn.click();       // only A fires; expired entry is pruned
}

Async self-reference without ownership extension

Inside a class that schedules callbacks, capturing this by raw pointer or by shared_ptr is dangerous β€” the former dangles, the latter creates a cycle. A weak_ptr capture is the correct pattern:

cpp
#include <memory>
#include <functional>

class Pipeline : public std::enable_shared_from_this<Pipeline> {  // C++11
public:
    void start() {
        std::weak_ptr<Pipeline> self = weak_from_this();           // C++17

        schedule_after(std::chrono::seconds(5), [self] {
            auto p = self.lock();
            if (!p) return;   // pipeline was already shut down β€” skip silently
            p->process_batch();
        });
    }

private:
    void process_batch();
    static void schedule_after(std::chrono::duration<long> d,     // C++11 chrono
                                std::function<void()> f);
};

Prior to C++17, weak_from_this() did not exist. The equivalent was:

cpp
std::weak_ptr<Pipeline> self(shared_from_this());  // C++11 workaround

Graph with shared ownership and non-owning edges

In a directed graph, each node may be pointed to by multiple owners, but back-edges and cross-edges should not prolong node lifetime:

cpp
#include <memory>
#include <vector>
#include <string>

struct GraphNode {
    std::string                              id;
    std::vector<std::shared_ptr<GraphNode>>  owned_edges;   // strong out-edges
    std::vector<std::weak_ptr<GraphNode>>    back_edges;    // non-owning; C++11

    explicit GraphNode(std::string s) : id(std::move(s)) {}

    void visit_back_neighbors(auto&& fn) const {            // abbreviated template β€” C++20
        for (const auto& wp : back_edges)
            if (auto n = wp.lock()) fn(*n);
    }
};

Best Practices

  • Always convert via lock(). weak_ptr intentionally provides no operator* or operator->. The shared_ptr returned by lock() is the only correct access path.
  • Check the result of lock() before use. Assign to a named variable and gate on it:
    cpp
    if (auto sp = wp.lock()) { sp->do_something(); }   // correct
  • Use weak_from_this() over constructing from shared_from_this() when C++17 is available. It avoids the temporary shared_ptr and the redundant strong-count round-trip.
  • Make the non-owning side of every bidirectional relationship a weak_ptr. Parent↔child, subject↔observer, registry↔entry β€” identify which direction drives lifetime and make the other direction weak.
  • Treat use_count() as diagnostic only. Its value may be stale by the time you act on it.

Common Pitfalls

Using expired() to gate access in concurrent code. The object can be destroyed between expired() returning false and the next lock() call. Always use lock() exclusively and test its result:

cpp
// Wrong in multithreaded code
if (!wp.expired()) { wp.lock()->method(); }   // TOCTOU race

// Correct
if (auto sp = wp.lock()) { sp->method(); }

Constructing from a raw pointer. There is no weak_ptr<T>(T*) constructor β€” a weak_ptr can only be constructed from a shared_ptr or another weak_ptr:

cpp
std::weak_ptr<Foo> wp(new Foo());   // error: no matching constructor

Calling shared_from_this() before the object is owned by a shared_ptr. This throws std::bad_weak_ptr. Stack-allocated instances and objects constructed directly (not via make_shared) will trigger it if shared_from_this() is called from a constructor or before a shared_ptr has taken ownership.

Holding the locked shared_ptr across suspension points. In coroutines or async callbacks, retaining the result of lock() across an co_await (C++20) extends the managed object's lifetime beyond the intended scope and can delay subscriber pruning. Lock only for the duration of the access, then release.

std::atomic<std::weak_ptr<T>> (C++20)

C++20 specializes std::atomic for both std::shared_ptr<T> and std::weak_ptr<T>, enabling atomic load, store, and compare-exchange operations that are safe across threads without external locking.

std::atomic<std::weak_ptr<T>> supports load(), store(), exchange(), compare_exchange_weak(), and compare_exchange_strong(). In practice is_always_lock_free is false on most implementations β€” the standard permits a library-internal lock β€” but the interface is still correct and data-race-free:

cpp
#include <atomic>
#include <memory>

struct Config { /* ... */ };

// C++20 β€” atomic weak_ptr slot for a hot-path cache
static std::atomic<std::weak_ptr<Config>> s_config_cache;

std::shared_ptr<Config> get_config() {
    // Atomically load the weak slot and promote
    if (auto p = s_config_cache.load().lock()) return p;

    auto fresh = std::make_shared<Config>(load_from_disk());
    // Store back; another thread may race here β€” that is fine,
    // the loser just drops its fresh copy
    std::weak_ptr<Config> expected{};
    s_config_cache.compare_exchange_strong(expected,
                                           std::weak_ptr<Config>(fresh));
    return fresh;
}

atomic<weak_ptr<T>> allows the object to be destroyed. This is the key contrast with std::atomic<std::shared_ptr<T>>: the atomic shared_ptr extends lifetime as long as any thread holds the slot; the atomic weak_ptr does not. Use the weak variant for caches where natural expiry is desirable, and the strong variant where you must guarantee the object survives the load.

Expiry Notification: Detecting Object Death

Sometimes observers need to react when an object disappears rather than just checking lazily on the next use. A burial token pattern uses the destructor of a shared_ptr as the notification event:

The observed object holds a std::shared_ptr<void> (or shared_ptr<monostate>) whose sole purpose is to be alive as long as the object is alive. Observers hold a std::weak_ptr<void> to the same control block. When the object's destructor runs, the shared_ptr member is destroyed, the strong count hits zero, and every weak_ptr::expired() immediately returns true.

cpp
#include <memory>

// C++11 β€” burial token pattern
class Widget {
public:
    Widget() : alive_token_(std::make_shared<void*>(nullptr)) {}

    // Hand out a weak handle that expires with this Widget
    std::weak_ptr<void> lifetime_token() const { return alive_token_; }

private:
    std::shared_ptr<void*> alive_token_;
};

class Listener {
public:
    explicit Listener(const Widget& w)
        : token_(w.lifetime_token()) {}

    bool widget_alive() const { return !token_.expired(); }

private:
    std::weak_ptr<void> token_;
};

Unlike polling use_count(), the burial token fires exactly when the destructor runs. It integrates with RAII: there is no window between "object is logically gone" and "observers detect it". The void (or void*) element type is deliberate β€” the token carries no data, only lifetime.

owner_before for Ordered Containers of weak_ptr

std::set and std::map require a strict weak ordering. Raw pointer comparison (operator< on the stored address) is undefined behavior across unrelated allocations and is useless for expired weak_ptr values where the address may no longer be meaningful. The correct comparator is std::owner_less, which orders by control block identity rather than pointed-to address:

cpp
#include <map>
#include <memory>
#include <set>

// C++11 β€” ordered set of weak_ptr by control block
using WeakSet = std::set<
    std::weak_ptr<Widget>,
    std::owner_less<std::weak_ptr<Widget>>
>;

// C++17 β€” transparent comparator: mix shared_ptr and weak_ptr in the same map
using Registry = std::map<
    std::weak_ptr<Widget>,
    int,
    std::owner_less<void>   // void specialisation β€” transparent; C++17
>;

owner_less is consistent across shared_ptr and weak_ptr referring to the same object. A shared_ptr<T> and a weak_ptr<T> constructed from it compare equivalent under owner_less<void>, which enables heterogeneous lookup without constructing a temporary weak_ptr.

To purge expired entries from an owner_less-keyed map, collect them first β€” erasing while iterating invalidates iterators:

cpp
// C++17 β€” purge expired entries
void purge_expired(Registry& reg) {
    std::vector<Registry::key_type> dead;
    for (auto& [wp, _] : reg)
        if (wp.expired()) dead.push_back(wp);
    for (auto& wp : dead)
        reg.erase(wp);
}

Control Block Lifetime in Depth

shared_ptr and weak_ptr implement two-phase destruction:

  1. Strong count β†’ 0: the managed object is destroyed (destructor called, memory potentially freed).
  2. Weak count β†’ 0: the control block itself is deallocated.

The control block stores the strong count, the weak count, the deleter (if custom), the allocator (if custom), and a type-erased pointer to the object. A weak_ptr that outlives the managed object keeps the control block alive but costs only the size of that block β€” typically 16–32 bytes on 64-bit platforms:

cpp
#include <memory>

// C++11 β€” illustrating control block lifetime
int main() {
    std::weak_ptr<int> wp;

    {
        auto sp = std::make_shared<int>(42);
        wp = sp;
        // strong count = 1, weak count = 1
    }
    // sp destroyed: strong count = 0  β†’  int destroyed
    // weak count is still 1 (wp holds it)
    // control block is ALIVE β€” wp.expired() is safe to call

    bool gone = wp.expired();  // true; control block still exists
    wp.reset();
    // weak count = 0 β†’ control block freed
}

make_shared trades deallocation flexibility for allocation efficiency. Because make_shared<T> places the object and control block in a single allocation, the control block cannot be freed until the last weak_ptr is gone β€” even though the object was destroyed when the strong count hit zero. The object's bytes sit inside the allocation, wasted but unreclaimable. Using shared_ptr<T>(new T(...)) allocates the object separately, so object memory is freed immediately when the strong count hits zero, while the control block waits for the weak count.

cpp
// C++11 β€” separate allocation: object freed at strong-count zero
auto sp = std::shared_ptr<Widget>(new Widget());
// ^ Widget allocation freed when sp dies, even if weak_ptrs remain
// make_shared<Widget>() would hold that memory until all weak_ptrs die

weak_ptr in Entity-Component Systems (ECS)

Game engines and simulation frameworks need lightweight references to entities that may be deleted at any time. Two common approaches are integer handles (entity ID + generation counter) and weak_ptr:

ApproachSizeValidity checkOverhead
Integer handle (ID + generation)4–8 bytes1–2 ns (array lookup + integer compare)None after check
std::weak_ptr<Entity>16 bytes5–10 ns (atomic load of strong count)Control block per entity
cpp
#include <memory>
#include <vector>

// C++11 β€” weak_ptr entity reference
using EntityRef = std::weak_ptr<Entity>;

class PhysicsSystem {
public:
    void track(std::shared_ptr<Entity> e) {
        tracked_.push_back(e);  // stores weak_ptr
    }

    void update() {
        // Prune dead entities, then process live ones
        std::erase_if(tracked_, [](const EntityRef& r) {
            return r.expired();
        });                                     // std::erase_if β€” C++20
        for (auto& ref : tracked_)
            if (auto e = ref.lock()) e->tick_physics();
    }

private:
    std::vector<EntityRef> tracked_;
};

For hot inner loops, prefer a hybrid: store both a weak_ptr (for validity) and a raw pointer (for access). Lock the weak_ptr once per frame boundary to verify the entity is still alive, then use the raw pointer for per-tick operations without paying the atomic cost on every access. Cache-invalidate the raw pointer whenever the weak_ptr expires.

Rebinding and Aliasing weak_ptr

A std::weak_ptr<T> can be constructed from any std::shared_ptr<T> or another std::weak_ptr<T> β€” both share the same control block and do not change the strong count. This extends to the shared_ptr aliasing constructor, which lets a shared_ptr own one control block but point to a different object:

cpp
#include <memory>

struct Packet {
    uint32_t   header;
    uint8_t    payload[256];
};

// C++11 β€” aliasing constructor: shared ownership of Packet,
// but the shared_ptr "points at" the payload member
auto sp_packet = std::make_shared<Packet>();
auto sp_payload = std::shared_ptr<uint8_t>(sp_packet, sp_packet->payload);
// sp_payload keeps Packet alive via sp_packet's control block,
// but sp_payload.get() == &sp_packet->payload

// weak_ptr inherits the aliased pointer
std::weak_ptr<uint8_t> wp_payload = sp_payload;  // C++11
// wp_payload.lock().get() == &sp_packet->payload

A weak_ptr constructed from an aliased shared_ptr shares the aliased shared_ptr's control block, and lock() returns a shared_ptr with the aliased pointer. This is useful when a subsystem needs a non-owning reference to a sub-object (a member, a base-class slice, an array element) without exposing the entire parent object:

cpp
#include <memory>

struct Config {
    int   debug_level = 0;
    // ... many other fields
};

// C++11 β€” weak_ptr to a single member; ownership stays with Config's refcount
std::weak_ptr<int> make_debug_level_ref(std::shared_ptr<Config> cfg) {
    auto sp_field = std::shared_ptr<int>(cfg, &cfg->debug_level);
    return std::weak_ptr<int>(sp_field);
}

The weak_ptr expires only when the owning Config object is fully destroyed β€” the aliased pointer to the member stays valid for exactly as long as the parent.

See Also

  • std::shared_ptr β€” the owning counterpart whose control block weak_ptr observes
  • std::enable_shared_from_this β€” CRTP base class providing shared_from_this() and weak_from_this()
  • std::unique_ptr β€” single-ownership smart pointer with no control block overhead
  • RAII β€” the ownership discipline underlying all smart pointers
  • Observer β€” pattern that heavily relies on weak_ptr for safe subscriber management