C++ / a working model

40 / 163   ·   C++11   ·   10 min

weak_ptr: Breaking Cycles and Safely Taking Temporary Ownership

Keep this sentence

weak_ptr observes an existing shared_ptr ownership group without increasing the strong-reference count, which suits back-pointers, subscribers, and non-owning caches. Always lock before access; the shared_ptr obtained on success protects the object's lifetime for that use. An expired check itself does not reserve the object.

In this lesson
  1. Why a Strong-Reference Cycle Is Not Collected Automatically
  2. lock Combines the Check with Taking Ownership
  3. Weak Observers Still Need Lifetime Design
  4. Example
  5. Exercise

Why a Strong-Reference Cycle Is Not Collected Automatically

If A owns B through a shared_ptr and B also owns A through a shared_ptr, each still has a strong owner after every external handle disappears. Both counts stay above zero, so neither destructor runs and the storage is not reclaimed. Reference counting is not tracing garbage collection. It does not notice that this group of objects as a whole has been cut off from the business root, so it will not break the cycle automatically. The cycle comes from the owning edges you chose.

First decide which edge owns the object in the domain and which edge is only a look-back. In the example the parent shared-owns the child, and the child observes the parent with a weak_ptr. After the external parent owner is released, the parent's destructor releases its child handle. Even if the outside still holds the child, the child's weak back-pointer will not forcibly keep the parent alive. That split is the point of the example: one strong owning edge, one non-owning observation.

lock Combines the Check with Taking Ownership

A weak_ptr cannot be dereferenced directly. auto p = weak.lock() atomically tries to obtain a strong reference in the same ownership group; if the object has already expired, the result is an empty shared_ptr. Store p as a local variable and use it only in the non-empty branch so the target's lifetime protection covers the entire use interval. That local shared_ptr is the temporary owner for this access.

Checking expired and then using a separately stored raw address leaves a window in which the object can be destroyed immediately after the check. expired is suitable for displaying status or pruning a cache, not for proving that the next access is safe. lock solves a lifetime race. It does not synchronize the object's fields, and it does not make arbitrary concurrent writes to the same weak_ptr variable safe. Treat the locked shared_ptr as the only handle you dereference.

Weak Observers Still Need Lifetime Design

When a weak cache stores only weak_ptr values, a cache entry expires as soon as the last true owner disappears; the next request must accept recreation. If the purpose of the cache is to force objects to remain, use an owning cache with capacity and an eviction policy instead of treating a weak cache as permanent storage. Weak observation answers whether the object is still there; it does not keep the object there.

The example accesses the object while temporarily locking the parent, then drops that temporary ownership, then releases the parent. The asserts show the parent is destroyed while the child still lives. The weak_ptr can remain and report expired. No usable parent object exists at that point, but control-block storage may still be retained until the weak observers also disappear. That leftover control block is bookkeeping for outstanding weak observers, not a live object.

Pitfalls

  • Changing a back-pointer to weak_ptr works only when every strong-reference cycle is cut; a lambda that captures a shared_ptr by value can also form a hidden cycle.
  • Each lock produces a new temporary strong owner; save the result once. Do not check that the first lock is non-empty and then dereference a second lock.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <memory>

struct Node {
    static int alive;
    std::shared_ptr<Node> child;
    std::weak_ptr<Node> parent;
    Node() { ++alive; }
    ~Node() { --alive; }
};
int Node::alive = 0;

int main() {
    auto parent = std::make_shared<Node>();
    auto child = std::make_shared<Node>();
    parent->child = child;
    child->parent = parent;
    {
        auto locked = child->parent.lock();
        assert(locked && locked->child == child);
    }
    parent.reset();
    assert(Node::alive == 1);
    assert(child->parent.expired());
    assert(!child->parent.lock());
    child.reset();
    assert(Node::alive == 0);
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-weak-ptr.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

An object stores a callback, and the callback captures that object's shared_ptr by value. How should this ownership relationship be rewritten?

Show a reference answer

Before installing the callback, construct a weak_ptr from the shared_ptr and let the callback capture the weak_ptr. When the callback runs, use if (auto self = weak.lock()) and access the object only in the success branch. Storing the callback then no longer adds a strong owner, so the object can be destroyed after the last external shared_ptr disappears, and a late callback simply skips its work.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog