C++ Circular References: shared_ptr Leaks and Breaking Cycles with weak_ptr
이 글의 핵심
Fix shared_ptr reference cycles: weak_ptr edges, lock() before use, enable_shared_from_this patterns for trees, caches, and observers—with leak detection tips.
Ownership tradeoffs: shared_ptr vs unique_ptr · smart pointers · leak detection.
Introduction: “I used shared_ptr but I still leak”
“The reference count never hits zero”
std::shared_ptr manages lifetime automatically, but cycles of shared_ptr keep strong counts ≥ 1, so destructors never run.
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // cycle: use weak_ptr for one direction
};
This article covers:
- What cycles are and how refcounting interacts
weak_ptrfundamentals- Patterns: parent/child, caches, observers
- Debugging leaks and suspicious counts
Table of contents
1. Circular references
Reference counting refresher
Copying shared_ptr increases the strong count; reset/destruction decreases it. At 0, the managed object is destroyed.
Cycle example (conceptual)
Two Person objects point to each other with shared_ptr. After function scope ends, each object is still reachable from the other → neither destructor runs.
2. weak_ptr
std::weak_ptr observes the same control block but does not increase the strong count.
auto p = std::make_shared<int>(42);
std::weak_ptr<int> w = p;
p.reset();
// w may be expired; use lock() before access
if (auto s = w.lock()) {
std::cout << *s << '\n';
}
Breaking cycles
Make one direction of a mutual association a weak_ptr (typically the “back-pointer” or observer side).
3. Practical patterns
Parent / child
- Own children with
shared_ptr(or unique ownership at leaves—design-dependent). - Child→parent as
weak_ptrwhen parent must not be kept alive solely by children.
Trees often combine enable_shared_from_this to pass shared_from_this() when registering a child.
Cache with weak values
Store weak_ptr<Resource> in a map keyed by name. On lookup, lock(); if expired, recreate and reinsert.
Observer lists
Observers as weak_ptr<Observer> so subjects do not keep observers alive forever; prune expired() entries periodically.
4. Debugging
- Log
use_count()when diagnosing surprising lifetimes. ~T()logging: if never called, suspect cycles or forgotten releases.- LeakSanitizer / Valgrind for heap leak reports.
Common mistakes (short)
- Doubly-linked list with
shared_ptrboth ways → makeprevaweak_ptr. - Publisher/listener mutual
shared_ptr→ listener holdsweak_ptr<Publisher>. - Child holds
shared_ptr<Parent>while parent holdsshared_ptr<Child>→ switch child toweak_ptr<Parent>unless you truly co-own.
Troubleshooting
Symptom: memory grows unbounded
Check use_count(), review mutual shared_ptr fields, run LSan/Valgrind.
Symptom: crash on shutdown
Using weak_ptr after destruction—always lock() and test for nullptr.
Performance note
weak_ptr::lock() involves atomic operations; usually fine compared to correctness. Prefer clear ownership DAGs over ad-hoc cycles.
Summary
Relationship cheat sheet
| Relation | Strong | Weak |
|---|---|---|
| Parent→child (owning) | shared_ptr / unique_ptr | — |
| Child→parent (non-owning) | — | weak_ptr |
| Cache value | — | weak_ptr |
| Observer | — | weak_ptr |
Rules
- Break cycles with
weak_ptron the non-owning edge. lock()before use; handle expiration.enable_shared_from_thiswhen a member must hand outshared_ptrto*this.
Checklist
- Any mutual
shared_ptrpairs? - Back-edges use
weak_ptr? -
lock()results checked? - Destructor logs confirm teardown in tests?
Related posts (internal)
- shared_ptr vs unique_ptr
- Memory leak detection
- Smart pointers guide
Keywords
circular reference, shared_ptr leak, weak_ptr, reference counting, LeakSanitizer
Practical tips
- Review any
shared_ptrfield pointing “up” or sideways in graphs. - Prefer
weak_ptrfor observers and caches. - Add CI tests that construct/destroy graphs and assert destructor side effects (where safe).
Closing
Circular shared_ptr graphs leak because strong counts never reach zero. Model ownership as a DAG, use weak_ptr for back-edges and observers, and validate with sanitizers and use_count() probes during development.
Next: Deepen with the full smart pointers article and memory leak tooling.