A std::weak_ptr<T> is a non-owning handle that watches an object managed by std::shared_ptr<T> without keeping it alive. It allows one part of the program to hold a reference to a shared object while leaving the decision of when to destroy that object entirely to the owners. This chapter covers what weak_ptr is, how it interacts with the control block, the two operations to use (lock and expired), common use cases, and how enable_shared_from_this lets a class safely hand out a shared_ptr to itself.
weak_ptr IsRecall from the previous chapter that every std::shared_ptr<T> carries two things: a raw pointer to the managed object, and a pointer to a small heap-allocated control block that stores reference counts. The control block tracks two separate counts. The strong count is the number of shared_ptr instances that own the object. When the strong count drops to zero, the object is destroyed. The weak count is the number of weak_ptr instances plus one (for as long as there are any shared_ptr instances).
A weak_ptr<T> holds a pointer to the same control block, but only increments the weak count. It does not contribute to the strong count, so it can never extend the object's lifetime by itself.
The weak_ptr named observer watched the same Product as owner. When owner.reset() ran, the strong count dropped from 1 to 0, the destructor fired, and the product was gone. The weak_ptr survived the destruction but now reports expired() == true. That is the entire idea: hold a handle that does not keep the object alive, and find out later whether it is still around.
A weak_ptr is always constructed from something that already shares ownership. The two normal sources are a shared_ptr and another weak_ptr.
A default-constructed weak_ptr does not refer to any control block. Calling expired() on it returns true, and lock() returns an empty shared_ptr. That is safe but useless until something is assigned into it.
Dereferencing a weak_ptr directly is not allowed. There is no operator*, no operator->, and no implicit conversion back to a raw pointer. The language enforces this because at the moment of a hypothetical dereference, the object might already have been destroyed by another thread or another scope. The only safe way to access the object is through lock(), which first promotes the weak reference to a strong one.
lock() and expired()These two operations are the way to interact with the underlying object through a weak_ptr.
lock() atomically checks whether the strong count is still greater than zero. If it is, lock produces a new std::shared_ptr<T> that participates in ownership for as long as the caller holds it, which guarantees the object stays alive during use. If the strong count is already zero (the object is gone), lock returns an empty shared_ptr.
expired() returns true if the strong count is zero. It is a cheap query and does not produce a shared_ptr. It exists mostly for diagnostics and quick branching when no access is about to happen.
lock() is the safe pattern. It returns a shared_ptr that can be checked with an if, and as long as that shared_ptr is in scope, the object cannot be destroyed even if every other strong owner releases it. That is the only way to use the object without a window where it might vanish.
A common but wrong pattern: if (!watcher.expired()) { auto p = watcher.lock(); ... }. Avoid it. Between the expired() check and the lock() call, another thread (or even a scoped destructor on the same thread) could release the last strong owner. The check would pass and the lock would still return an empty pointer. Always use the result of `lock()` directly.
lock() is more expensive than a raw pointer dereference because it touches the control block's reference counts atomically. It is still fast, but inside a tight inner loop where the object is certainly alive, hold a shared_ptr once at the top instead of calling lock() on every iteration.
weak_ptr HelpsThree places weak_ptr is useful in everyday code.
A wishlist is a list of products a customer is interested in. The catalog owns the products. If the wishlist held shared_ptr<Product> values, every wishlisted product would be kept alive by the wishlist itself, even after the catalog removed it. That defeats the catalog's authority over the product's lifetime. With weak_ptr, the wishlist watches the products without owning them.
The catalog deleted "Headphones" and the wishlist saw it the next time it tried to display. The wishlist did not need to subscribe to a removal event or be told ahead of time. It asked "is this still alive?" at the moment it needed the answer.
A cache that uses shared_ptr to its values keeps every cached entry alive forever, defeating the point. A cache built on weak_ptr returns the cached value if it is still alive somewhere else in the program, and otherwise rebuilds it. The cache acts as a fast lookup without holding entries hostage.
The cache held a weak_ptr to the product. While a and b were alive, the strong count was positive and lock() succeeded. Once they fell out of scope, the entry expired and the next request had to rebuild.
Trees, graphs, and any data structure with bidirectional links run into the same trap: parent owns child via shared_ptr, child also points back to parent via shared_ptr, and now neither can ever drop its count to zero. The strong counts hold each other up, the destructors never fire, and the whole structure leaks.
The fix is straightforward: pick the direction of ownership, use shared_ptr going one way, and use weak_ptr going the other. In a tree, parents own children via shared_ptr and children watch their parent via weak_ptr. Cycles cannot form because the weak count does not keep anything alive.
This chapter only sketches the problem; weak_ptr is the standard fix.
This is the part of weak_ptr that surprises most readers. When the strong count drops to zero, the object is destroyed (its destructor runs and its memory is freed if appropriate). But the control block sticks around as long as the weak count is non-zero. It has to: every existing weak_ptr still holds a pointer to it and still needs to call expired() or lock() safely.
In state 1, a shared_ptr and a weak_ptr both refer to the same control block, which in turn manages a live Product. The strong count is 1 and the weak count is 2 (one for the weak_ptr, plus one accounting for the fact that strong owners exist). When the shared_ptr is reset, the strong count hits zero and the Product is destroyed. The control block is still allocated, because the weak_ptr is still there and still needs to be queryable. Only when the last weak_ptr also goes away does the control block itself get freed.
There is a practical consequence. With std::make_shared, the control block and the object are allocated together as a single heap block. That block can only be freed when both counts are zero. Holding a weak_ptr long after every shared_ptr is gone keeps that combined block alive even though the object itself is dead. For tiny objects this does not matter; for very large objects, holding many long-lived weak_ptr instances can be a real memory concern.
enable_shared_from_thisA class sometimes needs to hand out a shared_ptr to itself. The classic situation is a member function that wants to register this somewhere as an observer, schedule a callback that calls back into this, or hand this to another object that expects shared ownership.
The naive attempt is to wrap this in a fresh shared_ptr and return it.
What is wrong with this code?
The shared_ptr returned by share() knows nothing about the existing shared_ptr named a. It creates a brand new control block with its own strong count of 1. Now two separate control blocks each believe they own the same Bad object. When b goes out of scope its control block deletes the Bad. When a goes out of scope its control block tries to delete the same Bad a second time. This is undefined behavior and typically crashes with a double-free.
The fix is std::enable_shared_from_this<T>. Inherit from it, and inside any member function call shared_from_this() to get a shared_ptr that shares the existing control block rather than inventing a new one.
Two rules apply when using enable_shared_from_this:
weak_ptr to this internally, and that weak pointer is wired up the first time a shared_ptr takes ownership of the object. Calling shared_from_this() before any shared_ptr exists throws std::bad_weak_ptr (since C++17). Earlier standards left it as undefined behavior.shared_ptr has not finished wiring itself to the object yet; during the destructor, the strong count is already zero, so there is nothing left to share.When designing a class whose objects will always live behind a shared_ptr, a common pattern is to make the constructor private and expose a static create() function that returns std::make_shared<MyClass>(...). That makes the precondition impossible to violate.
Inheriting from enable_shared_from_this adds the size of one weak_ptr to every instance of the class. For most classes this is negligible. For very small value types instantiated by the millions, it is worth being aware of.
10 quizzes