[2026] C++ shared_ptr Circular References: Parent/Child, Observer, Graph, Cache Patterns [#33-4]
이 글의 핵심
Master shared_ptr circular reference patterns: parent-child, observer, graph, cache. Complete examples, mistakes, best practices, and production patterns.
Introduction: Why Do shared_ptr Cycles Keep Breaking Things?
Real Production Scenarios
Scenario 1: Chat Server Room-Session Leak
In a chat server, Room held Session with shared_ptr, and Session referenced its Room with shared_ptr. When users left chat rooms, Room and Session held each other, preventing memory release. After 24 hours, thousands of “zombie” Rooms accumulated, causing OOM.
- Symptom: Process RSS in
top/htopincreases over time. Memory grows linearly with repeated room join/leave. - Root cause: After
Session::leave(), not removed fromRoom’sparticipants_, orSessionholdsRoomwith shared_ptr, creating cycle. - Solution: Change
Session→Roomtoweak_ptr. Design unidirectional ownership whereRoomownsSession. Scenario 2: DOM Tree Parser Memory Explosion HTML parser had nodes pointing at both parent and children withshared_ptr. After parsing large documents and discarding tree, parent↔child cycles prevented node release, accumulating hundreds of MB. - Symptom: After parsing 10MB HTML and releasing tree, process memory doesn’t decrease.
valgrind --leak-check=fullshows “no leaks” (OS reclaims on program exit). - Root cause:
child->parent = rootwith bidirectional shared_ptr. Whenrootreleases, onlychildrenrelease, butchildholdsroot, soroot’s refcount never reaches 0. - Solution: Change
parenttoweak_ptr. Parent owns children, children only “reference” parent. Scenario 3: Event Bus Subscriber Leak Event bus held subscribers asshared_ptr<Observer>. When widgets closed, they remained in subscription list, never releasing widgets. Subject owning Observer and Observer referencing Subject with shared_ptr created cycle. - Symptom: Closing dialogs doesn’t release memory. Even calling
unsubscribe, Subject’s shared_ptr keeps lifetime. - Root cause: Subject↔Observer bidirectional shared_ptr. When widget (Observer) closes, Subject’s
observersvector holds shared_ptr, keeping widget alive. - Solution: Subject holds subscribers as
weak_ptr. Innotify(), checkexpired()thenlock()to call only valid subscribers. Scenario 4: Graph Algorithm Node Leak Graph for pathfinding had nodes pointing at neighbors withshared_ptr. Bidirectional edges create A↔B cycles, and complex graphs have multiple nodes referencing each other, causing massive leaks. - Symptom: After pathfinding and releasing graph object, node destructors not called. 100K node graph leaks hundreds of MB.
- Root cause:
a->neighbors.push_back(b),b->neighbors.push_back(a)with bidirectional shared_ptr. Complex cycles like A↔B, B↔C, C↔A. - Solution: Make reverse direction
weak_ptr, or haveGraphown all nodes withunique_ptrand use indices/raw pointers for inter-node references. This article covers 4 typical patterns where shared_ptr circular references occur (parent-child, observer, graph, cache) with complete example code, common mistakes, best practices, and production patterns. What this covers: - Problem scenarios: Chat server, DOM, event bus, graph
- 4 cycle patterns: Parent-child, observer, graph, cache
- Complete examples: Before/After runnable code
- Common mistakes: use-after-free, lock failure, wrong direction choice
- Best practices: Ownership design, lock patterns
- Production patterns: Room-Session, resource cache, event system
Table of Contents
- Cycle Essence: Why Refcount Never Reaches 0
- Pattern 1: Parent-Child Tree
- Pattern 2: Observer Pattern
- Pattern 3: Graph Nodes
- Pattern 4: Resource Cache
- Common Mistakes and Solutions
- Best Practices and Design Principles
- Production Patterns
- Cycle Diagnosis and Debugging
- Performance: shared_ptr vs weak_ptr
- FAQ
- Interview Answers
- Checklist and Summary
Analogy
shared_ptr is like automatic cleaning robot with shared key—last person leaving triggers cleanup. weak_ptr is like contact in address book—doesn’t increase ownership, only checks with lock() if still connected when needed.
1. Cycle Essence: Why Refcount Never Reaches 0
shared_ptr Reference Count Behavior
shared_ptr maintains reference count. Copy increments +1, destruction/reset decrements -1. When 0, object is released.
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
flowchart LR
subgraph normal["Normal: Unidirectional Reference"]
M1[main] -->|shared_ptr| A1[A]
A1 -->|shared_ptr| B1[B]
note1["main releases → A:0 → B:0 sequential release"]
end
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
flowchart LR
subgraph circular["Cycle: Bidirectional shared_ptr"]
A2[A] -->|shared_ptr| B2[B]
B2 -->|shared_ptr| A2
note2["A ref:1 (B points)\nB ref:1 (A points)\n→ Never 0!"]
end
Key: Break One Side with weak_ptr
weak_ptr doesn’t increment refcount. Changing one direction to weak breaks cycle. 아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
flowchart LR
subgraph fixed[Break Cycle with weak_ptr]
A3[A] -->|shared_ptr| B3[B]
B3 -.->|weak_ptr| A3
note3["B→A doesn't increment count\n→ A can be released"]
end
Principle: Choose non-owning side as weak_ptr. If parent owns child → child→parent is weak. If Subject manages subscribers → Observer→Subject is weak.
2. Pattern 1: Parent-Child Tree
Problem: DOM, AST, Config Trees
Parent owns children with shared_ptr, child references parent with shared_ptr → cycle.
❌ Wrong: Bidirectional shared_ptr
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <iostream>
#include <string>
struct TreeNode {
std::string name;
std::shared_ptr<TreeNode> parent; // ❌ Parent as shared_ptr
std::vector<std::shared_ptr<TreeNode>> children;
TreeNode(const std::string& n) : name(n) {}
~TreeNode() { std::cout << "TreeNode '" << name << "' destroyed\n"; }
};
int main() {
auto root = std::make_shared<TreeNode>("root");
auto child = std::make_shared<TreeNode>("child");
root->children.push_back(child);
child->parent = root; // ❌ Cycle! root ref_count: 2, child ref_count: 2
std::cout << "root use_count: " << root.use_count() << "\n"; // 2
std::cout << "child use_count: " << child.use_count() << "\n"; // 2
} // Scope ends → destructors NOT called! Memory leak
Output:
root use_count: 2
child use_count: 2
(No destructor output “TreeNode destroyed” → memory leak)
✅ Correct: child→parent is weak_ptr
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <iostream>
#include <string>
struct TreeNode {
std::string name;
std::weak_ptr<TreeNode> parent; // ✅ Parent as weak_ptr
std::vector<std::shared_ptr<TreeNode>> children;
TreeNode(const std::string& n) : name(n) {}
~TreeNode() { std::cout << "TreeNode '" << name << "' destroyed\n"; }
std::shared_ptr<TreeNode> getParent() const {
return parent.lock();
}
};
int main() {
auto root = std::make_shared<TreeNode>("root");
auto child = std::make_shared<TreeNode>("child");
root->children.push_back(child);
child->parent = root; // ✅ weak_ptr → root ref_count: 1 maintained
std::cout << "root use_count: " << root.use_count() << "\n"; // 1
std::cout << "child use_count: " << child.use_count() << "\n"; // 2 (owned by root)
if (auto p = child->getParent()) {
std::cout << "child's parent: " << p->name << "\n";
}
} // root releases → children release → child releases → normal destruction
Output: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
root use_count: 1
child use_count: 2
child's parent: root
TreeNode 'child' destroyed
TreeNode 'root' destroyed
Using enable_shared_from_this (When Parent Adds Child)
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <algorithm>
struct TreeNode : std::enable_shared_from_this<TreeNode> {
std::string name;
std::weak_ptr<TreeNode> parent;
std::vector<std::shared_ptr<TreeNode>> children;
void addChild(std::shared_ptr<TreeNode> child) {
child->parent = std::weak_ptr<TreeNode>(shared_from_this());
children.push_back(std::move(child));
}
};
Explanation: shared_from_this() gets current object’s shared_ptr, assigns to child’s parent as weak_ptr. Parent owns children, children only “reference” parent, breaking cycle.
3. Pattern 2: Observer Pattern
Problem: Subject Owns Observer with shared_ptr
Subject holding subscribers as shared_ptr<Observer> causes cycle when Observer references Subject with shared_ptr. Also, even if Observer destructs first, Subject’s shared_ptr extends lifetime.
❌ Wrong: Bidirectional shared_ptr
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <iostream>
struct Subject;
struct Observer {
std::shared_ptr<Subject> subject; // ❌ Subject as shared_ptr
virtual void onEvent(const std::string& msg) = 0;
virtual ~Observer() { std::cout << "Observer destroyed\n"; }
};
struct Subject {
std::vector<std::shared_ptr<Observer>> observers; // Owns observers
void subscribe(std::shared_ptr<Observer> o) {
o->subject = std::shared_ptr<Subject>(this); // ❌ Dangerous + cycle
observers.push_back(std::move(o));
}
void notify(const std::string& msg) {
for (auto& o : observers) o->onEvent(msg);
}
};
Problems: Subject and Observer reference each other with shared_ptr. Also, shared_ptr<Subject>(this) is wrong (creates separate control block).
✅ Correct: Observer→Subject is weak_ptr
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>
struct Subject;
struct Observer {
std::weak_ptr<Subject> subject; // ✅ Subject as weak_ptr
std::string name;
virtual void onEvent(const std::string& msg) {
std::cout << "[" << name << "] " << msg << "\n";
}
virtual ~Observer() { std::cout << "Observer '" << name << "' destroyed\n"; }
};
struct Subject : std::enable_shared_from_this<Subject> {
std::vector<std::weak_ptr<Observer>> observers; // ✅ Hold as weak_ptr
void subscribe(std::shared_ptr<Observer> o) {
o->subject = weak_from_this();
observers.push_back(o);
}
void notify(const std::string& msg) {
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[](auto& w) { return w.expired(); }),
observers.end()
);
for (auto& w : observers) {
if (auto o = w.lock()) o->onEvent(msg);
}
}
};
int main() {
auto subject = std::make_shared<Subject>();
auto obs1 = std::make_shared<Observer>();
obs1->name = "Subscriber1";
subject->subscribe(obs1);
{
auto obs2 = std::make_shared<Observer>();
obs2->name = "Subscriber2";
subject->subscribe(obs2);
subject->notify("First notice");
} // obs2 destroyed → Subject doesn't keep it alive
subject->notify("Second notice"); // Only Subscriber1 receives
}
Output: 다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
[Subscriber1] First notice
[Subscriber2] First notice
Observer 'Subscriber2' destroyed
[Subscriber1] Second notice
Key: Subject holds subscribers as weak_ptr, so when Observer destructs first, Subject doesn’t extend lifetime. In notify(), remove expired() items and lock() to call only valid subscribers.
4. Pattern 3: Graph Nodes
Problem: Graphs with Bidirectional Edges
Nodes pointing at neighbors with shared_ptr create cycles with bidirectional edges like A↔B. Complex graphs have multiple nodes referencing each other, causing massive leaks.
❌ Wrong: Neighbors as shared_ptr
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <iostream>
struct Node {
int id;
std::vector<std::shared_ptr<Node>> neighbors; // ❌ Bidirectional = cycle
Node(int i) : id(i) {}
~Node() { std::cout << "Node " << id << " destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>(1);
auto b = std::make_shared<Node>(2);
a->neighbors.push_back(b);
b->neighbors.push_back(a); // ❌ Cycle! a ref:2, b ref:2
std::cout << "a use_count: " << a.use_count() << "\n"; // 2
std::cout << "b use_count: " << b.use_count() << "\n"; // 2
} // Destructors NOT called! No "Node 1 destroyed", "Node 2 destroyed"
Output:
a use_count: 2
b use_count: 2
(No destructor output → memory leak)
✅ Correct: Reverse Direction is weak_ptr
Define “ownership” direction in graph. Example: node with smaller ID → larger ID only shared_ptr, reverse is weak_ptr. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <iostream>
struct Node : std::enable_shared_from_this<Node> {
int id;
std::vector<std::shared_ptr<Node>> neighbors; // "Owned" neighbors
std::vector<std::weak_ptr<Node>> reverse_edges; // Reverse is weak
Node(int i) : id(i) {}
~Node() { std::cout << "Node " << id << " destroyed\n"; }
void addBidirectional(std::shared_ptr<Node> other) {
if (id < other->id) {
neighbors.push_back(other);
other->reverse_edges.push_back(shared_from_this());
} else {
other->neighbors.push_back(shared_from_this());
reverse_edges.push_back(other);
}
}
};
Simpler approach: One side weak_ptr. A→B as shared, B→A as weak. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <iostream>
struct Node;
struct Edge {
std::shared_ptr<Node> from;
std::weak_ptr<Node> to; // ✅ Reverse is weak
};
struct Node : std::enable_shared_from_this<Node> {
int id;
std::vector<std::shared_ptr<Edge>> outgoing;
Node(int i) : id(i) {}
~Node() { std::cout << "Node " << id << " destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>(1);
auto b = std::make_shared<Node>(2);
auto e = std::make_shared<Edge>();
e->from = a;
e->to = b;
a->outgoing.push_back(e);
// b doesn't own e → a releases → e releases → b releases
}
Output:
Node 2 destroyed
Node 1 destroyed
(Order: a releases → outgoing releases → e releases → b releases → a release completes)
Practical Pattern: External Graph Ownership
One container owns entire graph, inter-node references use raw pointers or indices. No cycles at all. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
struct Graph {
std::vector<std::unique_ptr<Node>> nodes; // Graph owns all nodes
// Inter-node references: indices or Node* (no ownership)
};
struct Node {
int id;
std::vector<size_t> neighbor_indices; // Reference by index
};
5. Pattern 4: Resource Cache
Problem: Cache Holding shared_ptr Never Releases
unordered_map<string, shared_ptr<Texture>> cache means once inserted, resource never releases. If you want “release when no longer used anywhere”, use weak_ptr.
❌ Wrong: shared_ptr Cache
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <unordered_map>
#include <string>
struct Texture {
std::string path;
Texture(const std::string& p) : path(p) {}
};
class TextureCache {
std::unordered_map<std::string, std::shared_ptr<Texture>> cache_; // ❌
public:
std::shared_ptr<Texture> get(const std::string& path) {
auto it = cache_.find(path);
if (it != cache_.end()) return it->second;
auto tex = std::make_shared<Texture>(path);
cache_[path] = tex; // Cache owns shared_ptr → never releases
return tex;
}
};
✅ Correct: weak_ptr Cache
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <unordered_map>
#include <string>
#include <iostream>
struct Texture {
std::string path;
Texture(const std::string& p) : path(p) {
std::cout << " Texture loaded: " << path << "\n";
}
~Texture() { std::cout << " Texture released: " << path << "\n"; }
};
class TextureCache {
std::unordered_map<std::string, std::weak_ptr<Texture>> cache_; // ✅
public:
std::shared_ptr<Texture> get(const std::string& path) {
auto it = cache_.find(path);
if (it != cache_.end()) {
if (auto tex = it->second.lock()) {
std::cout << "Cache hit: " << path << "\n";
return tex;
}
cache_.erase(it); // Remove expired entry
}
std::cout << "Cache miss: " << path << "\n";
auto tex = std::make_shared<Texture>(path);
cache_[path] = tex;
return tex;
}
};
int main() {
TextureCache cache;
{
auto tex1 = cache.get("grass.png"); // Miss → load
auto tex2 = cache.get("grass.png"); // Hit
} // tex1, tex2 release → Texture destroyed (cache only holds weak)
auto tex3 = cache.get("grass.png"); // weak expired → reload
}
Output: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Cache miss: grass.png
Texture loaded: grass.png
Cache hit: grass.png
Texture released: grass.png
Cache miss: grass.png
Texture loaded: grass.png
Explanation: Cache holds only weak_ptr, so when all external shared_ptrs disappear, resource auto-releases. Next get() call, lock() fails, remove from cache and reload.
6. Common Mistakes and Solutions
Mistake 1: Not Checking lock() Result
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ Dangerous: UB if lock() returns empty shared_ptr
std::weak_ptr<Guild> wp = getGuildWeak();
wp.lock()->sendMessage("hello"); // Crashes if wp expired!
// ✅ Safe
if (auto g = wp.lock()) {
g->sendMessage("hello");
}
Mistake 2: Wrong weak_ptr Direction
Non-owning side should be weak. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ Correct: Character doesn't own Guild
struct Character {
std::weak_ptr<Guild> guild;
};
struct Guild {
std::vector<std::shared_ptr<Character>> members; // Guild manages members
};
// ❌ Wrong: Guild holding members as weak? → Character deletes first, list just empty
// Depends on design, but "member list management" usually shared
Mistake 3: Trusting expired() Without lock()
In multithreading, object may be released between expired() check and lock().
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ Dangerous
if (!w.expired()) {
auto p = w.lock(); // p may be empty
}
// ✅ Safe: trust lock() result only
if (auto p = w.lock()) {
// p guaranteed valid
}
Mistake 4: Storing lock() Result in Member Long-Term
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ⚠️ Defeats weak_ptr purpose
class Handler {
std::shared_ptr<Service> service_; // Stores lock() result continuously
public:
void init(std::weak_ptr<Service> wp) {
service_ = wp.lock(); // Service lifetime extends to Handler's
}
};
// ✅ Recommended: lock() each time needed
void handle(std::weak_ptr<Service> wp) {
if (auto s = wp.lock()) {
s->doWork();
}
}
Mistake 5: Using shared_ptr(this)
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ Dangerous: creates new control block, possible double delete
void subscribe() {
subject_->addObserver(std::shared_ptr<Observer>(this));
}
// ✅ enable_shared_from_this + shared_from_this()
struct Observer : std::enable_shared_from_this<Observer> {
void subscribe(std::shared_ptr<Subject> s) {
s->addObserver(shared_from_this());
}
};
Mistake 6: Making All Reverse Directions weak in 3+ Cycles
Even with A→B→C→A cycle, breaking one edge suffices.
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<C> c; };
struct C { std::weak_ptr<A> a; }; // Only C→A weak is enough
Mistake 7: Missing enable_shared_from_this
Classes calling shared_from_this() must inherit enable_shared_from_this. Otherwise std::bad_weak_ptr exception occurs.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ Dangerous: shared_from_this() without enable_shared_from_this
struct BadNode {
void addChild(std::shared_ptr<BadNode> child) {
child->parent = shared_from_this(); // Compile error or bad_weak_ptr
}
};
// ✅ Correct
struct GoodNode : std::enable_shared_from_this<GoodNode> {
std::weak_ptr<GoodNode> parent;
void addChild(std::shared_ptr<GoodNode> child) {
child->parent = weak_from_this();
}
};
Mistake 8: Calling shared_from_this() in Constructor
Can’t call shared_from_this() before object is managed by shared_ptr. Calling in constructor causes bad_weak_ptr.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ Dangerous
struct Widget : std::enable_shared_from_this<Widget> {
Widget() {
auto self = shared_from_this(); // bad_weak_ptr! Not yet wrapped in shared_ptr
}
};
// ✅ Correct: call only after construction, after wrapped in shared_ptr
auto w = std::make_shared<Widget>();
w->setup(); // setup() internally uses shared_from_this()
Mistake 9: Capturing lock() Result Long-Term in Async Callback
Capturing lock() result in async callback and holding long-term defeats original weak_ptr lifetime management.
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ Dangerous: shared_ptr capture extends lifetime
void scheduleCallback(std::weak_ptr<Service> wp) {
if (auto sp = wp.lock()) {
scheduler.schedule([sp]() { // Captures sp → extends Service lifetime
sp->doWork();
});
}
}
// ✅ Recommended: lock() inside callback
void scheduleCallback(std::weak_ptr<Service> wp) {
scheduler.schedule([wp]() {
if (auto sp = wp.lock()) {
sp->doWork();
}
});
}
7. Best Practices and Design Principles
Principle 1: Define Ownership First
Clarify “who owns whom?”. Parent owns children, Subject owns subscriber list, cache only “references”.
Principle 2: Always Check lock() Result
Habituate if (auto p = wp.lock()) pattern. wp.lock()->foo() without check risks use-after-free.
Principle 3: expired() is Auxiliary
In multithreading, trust only lock() result. Use expired() only for filtering or statistics.
Principle 4: Clean Up Expired weak_ptrs
Periodically remove expired() items from observer lists, caches to reduce memory and traversal cost.
Principle 5: Default to unique_ptr, shared_ptr Only When Sharing Needed
shared_ptr has refcounting cost and cycle risk, so use only when “sharing truly needed”.
Principle 6: Document weak_ptr Usage
In team collaboration, comment “why this side is weak” to prevent mistakes during refactoring.
// Parent owns children, so child→parent is weak (prevent cycle)
std::weak_ptr<TreeNode> parent;
Principle 7: Cycle Possibility Review Checklist
When adding new bidirectional reference, verify:
- Does A point at B with shared_ptr?
- Does B point at A? (If shared_ptr, cycle!)
- Is non-owning side weak_ptr?
8. Production Patterns
Pattern 1: Chat Server Room-Session
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <set>
#include <string>
struct Session;
struct Room {
std::string id;
std::set<std::shared_ptr<Session>> participants_; // Room manages Sessions
void join(std::shared_ptr<Session> s);
void leave(std::shared_ptr<Session> s);
};
struct Session {
std::weak_ptr<Room> room_; // ✅ Session doesn't own Room
void send(const std::string& msg) {
if (auto r = room_.lock()) {
r->broadcast(msg);
}
}
};
Explanation: Room manages participants with shared_ptr, Session only references its Room with weak_ptr. When Session leaves, Room doesn’t hold Session, and when Room deletes, Session’s room_.lock() fails, enabling safe handling.
Pattern 2: Async Callback Lifetime Management
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void asyncFetch(std::weak_ptr<Widget> widget) {
fetchFromNetwork([widget](Response r) {
if (auto w = widget.lock()) {
w->onDataReceived(r); // Update if widget alive
}
// Ignore if widget already closed
});
}
Pattern 3: Game Character-Guild (Detailed Example)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <string>
#include <iostream>
struct Guild;
struct Character {
std::string name;
std::weak_ptr<Guild> guild; // ✅ Doesn't own guild
~Character() { std::cout << " Character '" << name << "' destroyed\n"; }
};
struct Guild {
std::string name;
std::vector<std::shared_ptr<Character>> members; // Guild manages members
~Guild() { std::cout << "Guild '" << name << "' destroyed\n"; }
};
int main() {
auto guild = std::make_shared<Guild>();
guild->name = "Brave Guild";
auto c1 = std::make_shared<Character>();
c1->name = "Warrior";
c1->guild = guild;
guild->members.push_back(c1);
std::cout << "guild use_count: " << guild.use_count() << "\n"; // 1
if (auto g = c1->guild.lock()) {
std::cout << c1->name << "'s guild: " << g->name << "\n";
}
} // guild releases → members release → c1 releases → normal destruction
Pattern 4: Event Bus Subscribers
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class EventBus {
struct Handler {
std::weak_ptr<void> target;
std::function<void(int)> callback;
};
std::vector<Handler> handlers;
public:
template<typename T>
void subscribe(std::shared_ptr<T> subscriber, void (T::*method)(int)) {
handlers.push_back({
std::weak_ptr<void>(subscriber),
[wp = std::weak_ptr<T>(subscriber), method](int v) {
if (auto s = wp.lock()) (s.get()->*method)(v);
}
});
}
void publish(int value) {
handlers.erase(
std::remove_if(handlers.begin(), handlers.end(),
[](auto& h) { return h.target.expired(); }),
handlers.end()
);
for (auto& h : handlers) h.callback(value);
}
};
Pattern 5: Plugin-Host Relationship
Plugin referencing host with weak_ptr is safe even if host releases plugin first.
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct PluginHost;
struct Plugin {
std::weak_ptr<PluginHost> host; // ✅ Doesn't own host
virtual void onLoad() = 0;
};
struct PluginHost {
std::vector<std::shared_ptr<Plugin>> plugins; // Host manages plugins
void callHostApi() {
for (auto& p : plugins) {
if (auto h = p->host.lock()) {
h->doSomething();
}
}
}
};
Pattern 6: Resource Manager + Users
Resource manager caches resources with weak_ptr, users own with shared_ptr. Auto-releases when usage ends. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class ResourceManager {
std::unordered_map<std::string, std::weak_ptr<Resource>> cache_;
public:
std::shared_ptr<Resource> load(const std::string& path) {
if (auto r = cache_[path].lock()) return r;
auto res = std::make_shared<Resource>(path);
cache_[path] = res;
return res;
}
void pruneExpired() {
for (auto it = cache_.begin(); it != cache_.end(); ) {
if (it->second.expired()) it = cache_.erase(it);
else ++it;
}
}
};
9. Cycle Diagnosis and Debugging
Diagnostic Flowchart
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TD
A[Suspect memory leak] --> B{Destructor called\nafter scope ends?}
B -->|Yes| C[Not cycle\nInvestigate other causes]
B -->|No| D[Check use_count]
D --> E{use_count > 1\nafter scope ends?}
E -->|Yes| F[Other shared_ptr referencing]
E -->|No| G[Check bidirectional references]
F --> H[Trace reference path]
G --> I[Change one side to weak_ptr]
H --> I
Check Suspect Area with use_count()
shared_ptr::use_count() checks refcount. If count ≥1 after leaving scope, something else holds reference.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void debugRefCount() {
auto obj = std::make_shared<MyObject>();
std::cout << "After creation: " << obj.use_count() << "\n"; // 1
other->hold(obj);
std::cout << "After other holds: " << obj.use_count() << "\n"; // 2
} // obj scope ends
// Should be 0 normally. ≥1 suggests cycle possibility
Valgrind Memory Leak Check
Valgrind may show “no leaks”. Cycles are allocated memory not freed, but OS reclaims on program exit, so Valgrind may not detect as “leak”. Suspect from long-running RSS increase. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Memory leak check
valgrind --leak-check=full ./my_app
# Monitor memory usage after 24-hour run
# Check top or /proc/self/status VmRSS
ASan + Manual Destruction Check
Add logs to destructors to verify “when object is released”. If destructor not called after leaving scope, suspect cycle. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
struct MyNode {
~MyNode() {
std::cout << "MyNode destroyed: " << id << "\n"; // If this doesn't print, cycle
}
};
GDB/LLDB Trace Reference Path
Examine shared_ptr control block to trace “who references this object”. (Implementation-dependent)
다음은 간단한 bash 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# LLDB inspect shared_ptr internals
(lldb) p my_obj
(lldb) p my_obj.__ptr_
(lldb) p my_obj.__cntrl_ # Control block address
Profiling to Identify Leak Area
If memory usage increases linearly during long runs, compare memory difference after repeating specific feature (room join, document parsing) to narrow suspect area.
10. Performance: shared_ptr vs weak_ptr
Operation Cost
| Operation | shared_ptr | weak_ptr |
|---|---|---|
| Copy | atomic ref_count++ | Control block access only |
| Destruction | atomic ref_count— | atomic weak_count— |
| lock() | — | atomic ref_count++, create shared_ptr |
| expired() | — | Check ref_count == 0 (atomic) |
lock() internally increments ref_count, so similar cost to shared_ptr copy. But storage itself is lighter than shared_ptr. Doesn’t affect object lifetime. |
Memory Usage
- shared_ptr: Object + control block (ref_count, weak_count, deleter, etc.)
- weak_ptr: Only references control block. Even after object release, control block remains while weak_ptr exists. Summary: For “store and occasionally access” patterns (observer lists, caches), weak_ptr is appropriate.
11. FAQ
Q. When do shared_ptr cycles occur?
A. When two objects point at each other with shared_ptr. Watch for bidirectional references in parent-child trees, observer pattern, graph nodes, caches.
Q. How to fix cycles?
A. Change one side to weak_ptr. Choose non-owning side (child→parent, observer→Subject, cache entry) as weak.
Q. weak_ptr vs raw pointer?
A. Raw pointers can’t tell “if pointed object is released”, causing undefined behavior (UB) on dereference. weak_ptr checks expiration with expired() and safely gets shared_ptr with lock(), eliminating dangling pointer risk.
Q. What about 3+ cycles?
A. Even with A→B→C→A cycle, breaking one edge with weak_ptr suffices. No need to make all reverse directions weak.
Q. When to use this in production?
A. Game servers (character-guild), GUI event subscriptions, DOM/AST trees, resource caches, chat servers (Room-Session)—break cycles with weak_ptr.
Q. What should I read first?
A. C++ Smart Pointers & Circular References for weak_ptr basics, C++ Smart Pointers for shared_ptr/unique_ptr fundamentals.
12. Interview Answers
Q: What is shared_ptr circular reference? How to fix?
“If A holds B with shared_ptr and B holds A with shared_ptr, they own each other so refcount never reaches 0, causing memory leak. This is circular reference. Fix by changing one side to weak_ptr. weak_ptr doesn’t increment count, breaking cycle, and use lock() to get shared_ptr only when needed.”
Q: When to use weak_ptr?
“Use to break circular references. When two objects point at each other with shared_ptr, refcount never reaches 0, causing leak. Changing one side to weak_ptr prevents count increment on that side, breaking cycle and enabling normal object release. Also use for reference-only relationships where ‘use if exists, ignore if not’. For example, in games, character holding guild with weak_ptr means if guild disbands, character doesn’t keep guild alive, and can use with lock() only when valid.”
Q: Difference between lock() and expired()?
”expired() only checks if object is released. lock() returns shared_ptr if valid, empty shared_ptr if expired. In multithreading, object may be released between expired() check and lock(), so judge only by lock() result for safety.”
13. Checklist and Summary
shared_ptr Cycle Resolution Checklist
- In bidirectional references, chose non-owning side as weak_ptr?
- After
lock()call, check return value? (if (auto p = wp.lock())) - Not directly accessing expired weak_ptr? (
wp.lock()->foo()❌) - In multithreading, not trusting expired() alone, judging by lock() result?
- In observer/cache, cleaning up expired items logic exists?
- Using enable_shared_from_this instead of
shared_ptr(this)?
Summary
| Pattern | Cycle Cause | Solution |
|---|---|---|
| Parent-child | Parent↔child shared_ptr | child→parent as weak_ptr |
| Observer | Subject↔Observer shared_ptr | Observer→Subject as weak_ptr, Subject holds subscribers as weak_ptr |
| Graph | Inter-node bidirectional shared_ptr | Reverse as weak_ptr or indices/raw pointers |
| Cache | Cache owns shared_ptr | Cache holds weak_ptr |
One-line summary: shared_ptr cycles break by changing one side to weak_ptr. Choose non-owning side as weak and always check lock() result. |
Related Articles
- C++ Smart Pointers & Circular Reference Solutions [#33-3]
- C++ Smart Pointers | Solving Circular Reference Bug Unfound for 3 Days
- C++ shared_ptr Advanced Guide | enable_shared_from_this, Aliasing
Keywords
shared_ptr circular reference, weak_ptr usage, memory leak solution, parent child tree shared_ptr, observer pattern weak_ptr, cache pattern weak_ptr, lock expired One-line summary: Break shared_ptr cycles by changing non-owning side to weak_ptr. Always check lock() result for safe access. Previous: C++ Smart Pointers & Circular Reference Solutions Next: Multithreading Data Race and Mutex/Atomic Note: #33-3 covers weak_ptr basics. This article (#33-4) focuses on complete examples and production patterns for 4 patterns.