Weak Pointers and Cyclic Memory Leaks (std::weak_ptr)
Smart Pointers and Memory Management

7.3 Weak Pointers and Cyclic Memory Leaks (std::weak_ptr)

While reference counting is excellent, it has a fatal flaw: circular references. If two objects hold shared pointers to each other, their reference counts can never hit zero, causing a permanent memory leak. C++ solves this by introducing std::weak_ptr.

Let's look at cyclic leaks and how weak pointers break the cycle.

The Circular Reference Leak Trap

Imagine a Parent class that holds a shared pointer to a Child, and the Child class holds a shared pointer back to the Parent. When the main function exits and destroys its shared pointers, the Parent still has a reference count of 1 (held by Child), and the Child has a reference count of 1 (held by Parent).

Because neither count can hit zero, their destructors are never called. The memory leaks permanently on the heap, despite using smart pointers!

Weak Pointers: The Non Owning Observer

A std::weak_ptr is a smart pointer that references a shared resource but does not increment the strong reference count. It only increments the weak reference count in the control block.

The Dual Integer Layout and Control Block Survival

Remember that the Control Block contains both a strong count and a weak count. When the strong count hits 0, the massive managed object (like a 10MB texture) is instantly deleted from the heap. However, the tiny Control Block itself is NOT deleted!

The Control Block must stay alive in RAM as long as the weak count is greater than 0, because the weak pointers need a place to check if the strong count has hit zero. The Control Block memory is only released when the weak count finally hits zero.

  • The Spectator Metaphor: A weak pointer is like a spectator looking at a house. You can observe it, but you do not own it. If the owners decide to demolish the house (the strong reference count hits zero), they do not need your permission. The house is destroyed, and you are left looking at an empty plot.

Accessing Data Safely with `lock()`

Because the underlying object can be destroyed at any moment, you cannot dereference a std::weak_ptr directly. You must call lock() to convert it back to a std::shared_ptr first. If the object has been destroyed, lock() returns a null shared pointer:

#include <iostream>
#include <memory>

struct Parent;

struct Child {
    // Resolve cycle by holding weak pointer back to Parent instead of shared pointer!
    std::weak_ptr<Parent> parentRef;
    ~Child() { std::cout << "Child destroyed\n"; }
};

struct Parent {
    std::shared_ptr<Child> childPtr;
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

int main() {
    {
        auto p{std::make_shared<Parent>()};
        auto c{std::make_shared<Child>()};
        
        p->childPtr = c;
        c->parentRef = p; // Cycle is broken!
    } // Both Parent and Child are destroyed successfully here!
    return 0;
}
Resolving circular leaks using std::weak_ptr.
Finished reading this lesson?