[2026] C++ shared_ptr Circular References: Parent/Child, Observer, Graph, Cache Patterns [#33-4]

[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/htop increases over time. Memory grows linearly with repeated room join/leave.
  • Root cause: After Session::leave(), not removed from Room’s participants_, or Session holds Room with shared_ptr, creating cycle.
  • Solution: Change SessionRoom to weak_ptr. Design unidirectional ownership where Room owns Session. Scenario 2: DOM Tree Parser Memory Explosion HTML parser had nodes pointing at both parent and children with shared_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=full shows “no leaks” (OS reclaims on program exit).
  • Root cause: child->parent = root with bidirectional shared_ptr. When root releases, only children release, but child holds root, so root’s refcount never reaches 0.
  • Solution: Change parent to weak_ptr. Parent owns children, children only “reference” parent. Scenario 3: Event Bus Subscriber Leak Event bus held subscribers as shared_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 observers vector holds shared_ptr, keeping widget alive.
  • Solution: Subject holds subscribers as weak_ptr. In notify(), check expired() then lock() to call only valid subscribers. Scenario 4: Graph Algorithm Node Leak Graph for pathfinding had nodes pointing at neighbors with shared_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 have Graph own all nodes with unique_ptr and 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

  1. Cycle Essence: Why Refcount Never Reaches 0
  2. Pattern 1: Parent-Child Tree
  3. Pattern 2: Observer Pattern
  4. Pattern 3: Graph Nodes
  5. Pattern 4: Resource Cache
  6. Common Mistakes and Solutions
  7. Best Practices and Design Principles
  8. Production Patterns
  9. Cycle Diagnosis and Debugging
  10. Performance: shared_ptr vs weak_ptr
  11. FAQ
  12. Interview Answers
  13. 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

Operationshared_ptrweak_ptr
Copyatomic ref_count++Control block access only
Destructionatomic 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

PatternCycle CauseSolution
Parent-childParent↔child shared_ptrchild→parent as weak_ptr
ObserverSubject↔Observer shared_ptrObserver→Subject as weak_ptr, Subject holds subscribers as weak_ptr
GraphInter-node bidirectional shared_ptrReverse as weak_ptr or indices/raw pointers
CacheCache owns shared_ptrCache 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.

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.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3