본문으로 건너뛰기
Previous
Next
C++ Circular References: shared_ptr Leaks and Breaking

C++ Circular References: shared_ptr Leaks and Breaking

C++ Circular References: shared_ptr Leaks and Breaking

이 글의 핵심

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](/en/blog/cpp-comparison-04-shared-unique-ptr/ · [smart pointers](/en/blog/cpp-smart-pointers/ · [leak detection](/en/blog/cpp-error-07-memory-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_ptr fundamentals
  • Patterns: parent/child, caches, observers
  • Debugging leaks and suspicious counts

Table of contents

  1. What is a circular reference?
  2. weak_ptr basics
  3. Practical patterns
  4. Debugging
  5. Summary

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_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 in a map keyed by name. On lookup, lock(); if expired, recreate and reinsert.

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_ptr both 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

RelationStrongWeak
Parent→child (owning)shared_ptr / unique_ptr
Child→parent (non-owning)weak_ptr
Cache valueweak_ptr
Observerweak_ptr

Rules

  1. Break cycles with weak_ptr on the non-owning edge.
  2. lock() before use; handle expiration.
  3. enable_shared_from_this when a member must hand out shared_ptr to *this.

Checklist

  • Any mutual shared_ptr pairs?
  • Back-edges use weak_ptr?
  • lock() results checked?
  • Destructor logs confirm teardown in tests?


Keywords

circular reference, shared_ptr leak, weak_ptr, reference counting, LeakSanitizer

Practical tips

  • Review any shared_ptr field 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).

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.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Why shared_ptr cycles leak memory, how weak_ptr breaks cycles, parent/child and cache/observer patterns, use_count debug… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete](/en/blog/cpp-comparison-04-shared-unique-ptr/
  • [Finding C++ Memory Leaks: Valgrind, AddressSanitizer, and](/en/blog/cpp-error-07-memory-leak-detection/
  • [C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe](/en/blog/cpp-smart-pointers/

이 글에서 다루는 키워드 (관련 검색어)

C++, Circular reference, shared_ptr, weak_ptr, Memory leak, Smart pointers 등으로 검색하시면 이 글이 도움이 됩니다.