LESSON 15 · INTERMEDIATE4 min read

Cyclic References, Memory Leaks, and std::weak_ptr

Diagnose circular reference memory leaks in graph and observer patterns, and break reference cycles using std::weak_ptr and the lock() promotion idiom.

C++20 / C++23Free to readModule 4 of 11
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Diagnose fatal memory leaks caused by circular std::shared_ptr references in node graphs and observer patterns
  • Implement std::weak_ptr to create non-owning observers that do not increment the strong reference count
  • Use weak_ptr::lock() to safely promote weak references to temporary shared_ptr instances before access

01The Circular Reference Dilemma

While std::shared_ptr automates memory management, it suffers from a fatal blind spot: Circular References (Reference Cycles). A circular reference occurs when two or more objects hold std::shared_ptr references to each other.

Consider a Node A and Node B. Node A has a shared_ptr to Node B, and Node B has a shared_ptr to Node A. When the outer stack pointers go out of scope, both nodes still have a strong reference count of 1 because each node is kept alive by the other!

Neither destructor can ever execute. The memory is leaked permanently for the lifetime of the process. In complex graph topologies, tree parent pointers, and observer subscriber lists, circular references are the number one cause of memory leaks in modern C++.

example.js⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02std::weak_ptr: The Non-Owning Observer

std::weak_ptr<T> solves circular references by acting as a non-owning observer. A weak_ptr points to an object managed by a shared_ptr, but it does NOT increment the strong reference count.

Instead, a weak_ptr increments only the weak reference count in the Control Block. Because it does not contribute to the strong count, it does not prevent the managed object from being deleted when all strong shared_ptr owners go out of scope.

In graph structures, parent pointers, cache entries, and observer lists, replacing mutual shared_ptr links with std::weak_ptr breaks reference cycles completely.

03The lock() Promotion Idiom

Because a std::weak_ptr does not own the object, the underlying object could be deleted by another thread at any moment. Therefore, std::weak_ptr intentionally does not provide operator-> or operator* dereference operators.

To access the object safely, you must promote the weak_ptr to a temporary std::shared_ptr using weak.lock(). The lock() operation is thread-safe and atomic:

If the object is still alive, lock() returns a valid shared_ptr (incrementing the strong count temporarily so the object cannot be deleted while in use). If the object has already been destroyed, lock() returns an empty shared_ptr (evaluating to nullptr).

example.js⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Weak Pointer State Transition Table

How strong and weak reference counts evolve across object lifetimes:

The control block remains in memory until both strong count AND weak count reach zero.

notes.txtPlain text
+-------------------------+--------------+------------+------------------------+| Operation               | Strong Count | Weak Count | Object Status          |+-------------------------+--------------+------------+------------------------+| auto sp = make_shared   | 1            | 0          | Alive & Active         || weak_ptr wp = sp        | 1            | 1          | Alive & Observed       || sp.reset()              | 0            | 1          | Destructed (~T() runs!)|| wp.lock() (after reset) | 0            | 1          | Returns nullptr        || wp.reset()              | 0            | 0          | Control Block Freed!   |+-------------------------+--------------+------------+------------------------+

05Production Systems Architecture & Hardware Trade-Offs

From an operating system and hardware architecture perspective, systems software must account for physical execution realities that compiler abstractions intentionally hide. At the hardware level, instruction caches (I-Cache) and data caches (D-Cache) operate independently, and CPU pipeline front-ends rely heavily on branch target buffers (BTB) to minimize instruction fetch stalls. When code is organized into coherent, spatially contiguous blocks within translation units, binary code density increases, improving instruction cache hit rates across high-frequency execution loops.

Furthermore, modern linkers such as LLVM lld and mold perform Link-Time Optimization (LTO) across translation units. When LTO is enabled (-flto), the compiler writes LLVM bitcode into object files rather than final machine code, allowing the linker to perform cross-translation-unit function inlining, devirtualization, and dead-code stripping across the entire binary.

LET IT CLICK

The ideas worth keeping.

  • Circular references between std::shared_ptr instances cause permanent memory leaks.
  • std::weak_ptr provides non-owning observation without incrementing the strong reference count.
  • Always use weak.lock() to safely promote a weak pointer to a temporary shared pointer before access.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Exercise 1: Write an Observer pattern subscription struct

Write an Observer pattern subscription struct where Subject holds a std::vector<std::weak_ptr<Observer>> to notify listeners without preventing their destruction.

your_turn_1.cpp⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.cppC++
struct Observer { virtual void on_event() = 0; virtual ~Observer() = default; };#include <vector>#include <memory>class EventSubject {private:    std::vector<std::weak_ptr<Observer>> observers_;public:    void register_observer(std::weak_ptr<Observer> obs) {        observers_.push_back(obs);    }    void notify_all() {        for (auto it = observers_.begin(); it != observers_.end(); ) {            if (auto obs = it->lock()) {                obs->on_event();                ++it;            } else {                // Prune dead observers automatically!                it = observers_.erase(it);            }        }    }};

Use it->lock() to check if observer is alive, and erase expired observers.

02

Exercise 2: Why does std weak ptr not provide direct ope

Why does std::weak_ptr not provide direct operator* or operator-> dereference operators?

your_turn_2.cpp⚙️C++
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.cppC++
// If weak_ptr allowed direct dereferencing, another thread could destroy the object// between the time you check if it exists and the time you read its fields (a race condition).// By requiring weak.lock(), C++ forces the creation of a temporary shared_ptr that atomically// increments the strong reference count, guaranteeing the object cannot be destroyed during access.

Think about concurrency: what if another thread destroys the object during dereferencing?

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What causes a memory leak when two objects reference each other with std::shared_ptr?
02How do you check if the object pointed to by a std::weak_ptr is still alive and access it safely?
03Does creating a std::weak_ptr increment the strong reference count?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Exercise 1: Write an Observer pattern subscription struct

Write an Observer pattern subscription struct where Subject holds a std::vector<std::weak_ptr<Observer>> to notify listeners without preventing their destruction.

EXPECTED OUTPUT
Build completed. Binary executed.
[Exit code: 0]
main.cppC++
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.