본문으로 건너뛰기 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 · 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_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.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Why shared_ptr cycles leak memory, how weak_ptr breaks cycles, parent/child and cache/observer patterns, use_count.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, Circular reference, shared_ptr, weak_ptr, Memory leak, Smart pointers.