[2026] C++ Circular References: shared_ptr Leaks and Breaking Cycles with weak_ptr
이 글의 핵심
Why shared_ptr cycles leak memory, how weak_ptr breaks cycles, parent/child and cache/observer patterns, use_count debugging, Valgrind, and ASan LeakSanitizer.
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. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 타입 정의
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. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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_ptr when 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
Observer lists
Observers as weak_ptr 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 → make prev a weak_ptr. - Publisher/listener mutual
shared_ptr→ listener holds weak_ptr. - Child holds
shared_ptr<Parent>while parent holds shared_ptr→ switch child to weak_ptr 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_ptr on the non-owning edge.
- lock() before use; handle expiration.
- enable_shared_from_this when a member must hand out
shared_ptrto*this.
Checklist
- Any mutual shared_ptr pairs?
- Back-edges use weak_ptr?
- lock() results checked?
- Destructor logs confirm teardown in tests?
Related posts (internal)
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_ptr for observers and caches.
- Add CI tests that construct/destroy graphs and assert destructor side effects (where safe).