[2026] C++ Smart Pointers & Breaking Circular References with weak_ptr [#33-3]

[2026] C++ Smart Pointers & Breaking Circular References with weak_ptr [#33-3]

이 글의 핵심

Complete guide to solving circular reference memory leaks with weak_ptr. Learn lock(), expired(), observer pattern, cache pattern, and production patterns with real-world examples.

Introduction: Memory Leaks from Circular References

Real Production Scenarios

Scenario 1: MMORPG Server Memory Leak While developing an MMORPG server, we discovered memory usage continuously increasing over time. Even after players logged out, character and guild objects weren’t freed, accumulating several GB of memory over 24 hours. The cause: character ↔ guild mutual shared_ptr references creating circular references. Scenario 2: GUI Event Handler Leaks In Qt or custom GUI frameworks, widgets registered with an event bus remained in the subscriber list as shared_ptr even after closing, preventing widget deallocation. Parent-child widgets referencing each other or event publishers owning subscribers extended lifetimes indefinitely. Scenario 3: Resource Cache Unbounded Growth A game engine cached textures and models in unordered_map<string, shared_ptr<Texture>>, but once loaded, resources never freed, causing continuous memory accumulation. We wanted automatic deallocation when “no longer in use anywhere,” but the cache holding shared_ptr kept lifetimes infinite. Scenario 4: Parent-Child Tree Structures DOM trees, script engine object graphs, config parser node trees—when parent references child and child references parent with shared_ptr, cycles occur. Without weak_ptr for bidirectional links needed for tree traversal, memory leaks are inevitable. Scenario 5: Plugin/Module Systems Plugin managers storing loaded plugins as shared_ptr while plugins reference the manager create cycles. Even after unloading, managers holding shared_ptr prevent plugin deallocation. Switching plugin lists to weak_ptr enables automatic expiration on plugin release. Scenario 6: Network Sessions & Connection Pools Servers managing client sessions with shared_ptr while sessions reference the server create cycles. Even after disconnection, servers owning sessions prevent connection cleanup. Using weak_ptr for session lists enables natural cleanup on client disconnect. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ Problem code: circular reference causes memory leak
// 타입 정의
struct Character;
struct Guild {
    std::vector<std::shared_ptr<Character>> members;  // Guild owns characters
};
struct Character {
    std::shared_ptr<Guild> guild;  // Character owns guild
};
// Both shared_ptr → mutual "ownership" → refcount never hits 0 → leak!

Understanding unique_ptr as “single ownership” and shared_ptr as “shared via reference counting,” weak_ptr is “a pointer that doesn’t increment refcount, allowing access if the pointed object is alive”—key for breaking circular references. What this guide covers: Why circular references cause leaks, how weak_ptr solves it, lock()/expired() usage, observer/cache patterns, common mistakes, performance comparison, production patterns (event systems, resource caches).

Table of Contents

  1. shared_ptr and Reference Count Review
  2. Circular References and Memory Leaks
  3. Breaking Cycles with weak_ptr
  4. lock() and expired() Detailed Examples
  5. weak_ptr Usage Patterns: Observer & Cache
  6. Common Mistakes and Solutions 6.5. weak_ptr Best Practices
  7. Performance Comparison: shared_ptr vs weak_ptr
  8. Production Patterns: Event Systems & Resource Caches
  9. When to Use weak_ptr: Character and Guild
  10. Interview Answers

Conceptual Analogy

shared_ptr is like a shared key with automatic cleanup—when the last person leaves, resources are cleaned up. weak_ptr is like a contact in an address book—it doesn’t increase ownership, and you lock() only when needed to check if still connected.

1. shared_ptr and Reference Count Review

  • shared_ptr shares the same target across multiple locations, freeing the object when no shared_ptr points to it.
  • Internally maintains reference count: +1 on copy, -1 on destruction/reset. When it hits 0, delete is called.
  • Problem: If two objects point to each other with shared_ptr, the count never reaches 0, causing memory leaks.

2. Circular References and Memory Leaks

What is a circular reference?

Circular reference occurs when object A points to B, and B points back to A. If both use shared_ptr, they “mutually own” each other, preventing reference counts from reaching zero.

Circular reference diagram

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart LR
    subgraph circular["Circular Reference (shared_ptr only)"]
        A1[A] -->|shared_ptr| B1[B]
        B1 -->|shared_ptr| A1
        A1 -.->|ref_count: 2| A1
        B1 -.->|ref_count: 2| B1
    end

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart TB
    subgraph refcount[Reference Count Flow]
        M[main: pa, pb] --> A[A object]
        M --> B[B object]
        A -->|pa->b = pb| B
        B -->|pb->a = pa| A
    end
    note["When pa, pb go out of scope in main\nEach count -1\n→ A: 1 (B points to it)\n→ B: 1 (A points to it)\n→ Never reaches 0!"]

Complete circular reference example

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
struct B;
struct A {
    std::shared_ptr<B> b;
    ~A() { std::cout << "A destroyed\n"; }
};
struct B {
    std::shared_ptr<A> a;
    ~B() { std::cout << "B destroyed\n"; }
};
int main() {
    auto pa = std::make_shared<A>();
    auto pb = std::make_shared<B>();
    pa->b = pb;   // A owns B
    pb->a = pa;   // B owns A → cycle!
    std::cout << "A ref_count: " << pa.use_count() << "\n";  // 2
    std::cout << "B ref_count: " << pb.use_count() << "\n";   // 2
}  // pa, pb go out of scope → count -1 each → A:1, B:1 → destructors never called!

Output:

A ref_count: 2
B ref_count: 2

(Destructors “A destroyed”, “B destroyed” never print → memory leak)

Breaking cycles with weak_ptr (diagram)

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart LR
    subgraph fixed[Breaking cycle with weak_ptr]
        A2[A] -->|shared_ptr| B2[B]
        B2 -.->|weak_ptr| A2
        note2["B→A doesn't increment count\n→ A ref_count: 1 only"]
    end

Making one direction weak_ptr prevents that direction from incrementing the reference count, breaking the cycle.

Circular reference prevention: Before/After

Before (shared_ptr only): 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ Circular reference → memory leak
struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // prev points to next, next points to prev
};
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1;  // n1↔n2 cycle, refcount never hits 0

After (breaking with weak_ptr): 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ Reverse direction as weak_ptr → cycle broken
// 타입 정의
struct Node {
    std::shared_ptr<Node> next;   // Forward: ownership
    std::weak_ptr<Node> prev;     // Backward: reference only
};
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1;  // prev doesn't increment count → n1 count stays 1
// When n1 freed, n1→n2 breaks → n2 count 0 → proper deallocation

Selection criteria: Ask “does this object own that object?” If not owning, use weak_ptr. In lists, next “owns the next node” while prev “only references previous,” so make prev weak.

3. Breaking Cycles with weak_ptr

What is weak_ptr?

  • A pointer that can point to objects managed by shared_ptr without “owning” them.
  • Doesn’t increment reference count. Making the “shouldn’t keep object alive” direction weak_ptr prevents count increase in that direction, breaking the cycle.

Usage

  • lock(): Returns shared_ptr if object still alive, empty shared_ptr if already freed. Perfect for “use if available, ignore if not” patterns.
  • expired(): Only checks if already freed. lock() usage example: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::weak_ptr<Guild> my_guild = character->getGuildWeak();
if (auto g = my_guild.lock()) {
    // Guild still alive → access via g
    g->sendMessage("hello");
} else {
    // Already disbanded → handle "no guild"
}

Code explanation: lock() returns shared_ptr, so if (auto g = my_guild.lock()) enters the block only when g is valid. If expired, lock() returns empty shared_ptr evaluating to false, jumping to else. Master this pattern for easy weak_ptr usage. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct B;
struct A {
    std::shared_ptr<B> b;
};
struct B {
    std::weak_ptr<A> a;  // Doesn't own A → cycle broken
};
auto pa = std::make_shared<A>();
auto pb = std::make_shared<B>();
pa->b = pb;
pb->a = pa;  // weak_ptr, so A's refcount is 1 (only main's pa)
// When main drops pa, A count 0 → A freed → pa->b(pb) freed → B count 0 → B freed

Since B only points to A via weak_ptr, B doesn’t have “ownership of A”. When main drops pa, A is freed first, and A’s b (shared_ptr to B) disappears, making B’s count 0, finally freeing B. Cycle broken, leak eliminated.

Complete circular reference solution: Character and Guild

Below is complete runnable code comparing before/after weak_ptr application. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
#include <vector>
#include <string>
struct Guild;
struct Character {
    std::string name;
    std::weak_ptr<Guild> guild;  // ✅ weak_ptr: 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 = "Valor Guild";
    auto c1 = std::make_shared<Character>();
    c1->name = "Warrior";
    c1->guild = guild;
    guild->members.push_back(c1);
    auto c2 = std::make_shared<Character>();
    c2->name = "Mage";
    c2->guild = guild;
    guild->members.push_back(c2);
    std::cout << "guild use_count: " << guild.use_count() << "\n";  // 1 (members only)
    // Access guild info (using lock)
    if (auto g = c1->guild.lock()) {
        std::cout << c1->name << "'s guild: " << g->name << "\n";
    }
    std::cout << "--- Scope ending ---\n";
}  // guild, c1, c2 freed → proper destruction in order

Output: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

guild use_count: 1
Warrior's guild: Valor Guild
--- Scope ending ---
Guild 'Valor Guild' destroyed
  Character 'Warrior' destroyed
  Character 'Mage' destroyed

Explanation: Character::guild being weak_ptr doesn’t increment guild’s reference count. Guild::members is shared_ptr, but this relationship is unidirectional “guild manages member list” ownership. Characters only weakly reference guild, breaking the cycle, and all objects properly destruct on scope exit.

4. lock() and expired() Detailed Examples

lock() — Obtaining valid shared_ptr

lock() returns shared_ptr if the object weak_ptr points to is still alive, empty shared_ptr if already freed. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
// 타입 정의
struct Guild {
    std::string name;
    void sendMessage(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << "\n";
    }
};
struct Character {
    std::weak_ptr<Guild> guild;
};
int main() {
    auto guild = std::make_shared<Guild>();
    guild->name = "Valor Guild";
    Character c;
    c.guild = guild;
    // ✅ Access only when valid with lock()
    if (auto g = c.guild.lock()) {
        g->sendMessage("hello");  // [Valor Guild] hello
    } else {
        std::cout << "Guild disbanded.\n";
    }
    guild.reset();  // Free guild
    if (auto g = c.guild.lock()) {
        g->sendMessage("hello");  // Not executed
    } else {
        std::cout << "Guild disbanded.\n";  // Enters here
    }
}

Key point: The if (auto g = weak.lock()) pattern enters the block only when g is valid. If expired, lock() returns empty shared_ptr evaluating to false, jumping to else.

expired() — Checking expiration only

Use when you only need to check “already freed?” without accessing the object. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
void notifyMembers(const std::vector<std::weak_ptr<Character>>& members) {
    for (const auto& w : members) {
        if (w.expired()) {
            std::cout << "Member already left\n";
            continue;
        }
        if (auto c = w.lock()) {
            c->receiveNotification("Announcement");
        }
    }
}

Caution: Even if expired() returns false, another thread might free the object before the next lock() call. In multithreaded environments, trust only lock() results, using expired() only as a “rough filter.” 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ Dangerous: object can be freed between expired() check and lock()
if (!w.expired()) {
    // Another thread might free object here!
    auto p = w.lock();  // p might be empty
}
// ✅ Safe: judge only by lock() result
if (auto p = w.lock()) {
    // p guaranteed valid
}

use_count() with weak_ptr

weak_ptr can report current shared_ptr count via use_count(). 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
std::cout << "use_count: " << wp.use_count() << "\n";  // 1
sp.reset();
std::cout << "expired: " << wp.expired() << "\n";      // true

Complete lock() and expired() example

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
#include <vector>
struct Player {
    std::string name;
    Player(const std::string& n) : name(n) {}
    ~Player() { std::cout << "  Player '" << name << "' destroyed\n"; }
};
int main() {
    std::vector<std::weak_ptr<Player>> list;
    auto p1 = std::make_shared<Player>("Alice");
    auto p2 = std::make_shared<Player>("Bob");
    list.push_back(p1);
    list.push_back(p2);
    // Process only valid entries with lock()
    for (size_t i = 0; i < list.size(); ++i) {
        if (auto p = list[i].lock())
            std::cout << "[" << i << "] " << p->name << " (valid)\n";
        else
            std::cout << "[" << i << "] (expired)\n";
    }
    p1.reset();  // Free p1
    std::cout << "After p1 reset, list[0].expired(): " << list[0].expired() << "\n";
    if (auto p = list[0].lock())
        std::cout << "[0] " << p->name << "\n";
    else
        std::cout << "[0] (expired)\n";
    if (auto p = list[1].lock())
        std::cout << "[1] " << p->name << " (valid)\n";
}

Output: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

[0] Alice (valid)
[1] Bob (valid)
After p1 reset, list[0].expired(): 1
[0] (expired)
[1] Bob (valid)
  Player 'Alice' destroyed
  Player 'Bob' destroyed

Explanation: After p1.reset(), list[0] expires and lock() returns empty shared_ptr. Since list only holds weak_ptr, it doesn’t affect player lifetimes.

5. weak_ptr Usage Patterns: Observer & Cache

Observer Pattern

When subscribers (Observers) reference publishers (Subjects), publishers shouldn’t own subscribers. Maintain subscriber lists as weak_ptr, automatically filtering deleted subscribers. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
#include <algorithm>
struct Observer {
    virtual void onEvent(const std::string& msg) = 0;
    virtual ~Observer() = default;
};
struct Subject {
    std::vector<std::weak_ptr<Observer>> observers;
    void subscribe(std::shared_ptr<Observer> o) {
        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);
            }
        }
    }
};

Complete observer pattern example (runnable): 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>
struct Observer {
    std::string name;
    virtual void onEvent(const std::string& msg) {
        std::cout << "[" << name << "] Event received: " << msg << "\n";
    }
    virtual ~Observer() { std::cout << "Observer '" << name << "' destroyed\n"; }
};
struct Subject {
    std::vector<std::weak_ptr<Observer>> observers;
    void subscribe(std::shared_ptr<Observer> o) {
        observers.push_back(o);
    }
    void notify(const std::string& msg) {
        // Remove expired subscribers
        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 announcement");  // Both receive
    }  // obs2 scope ends → destroyed
    subject->notify("Second announcement");  // Only Subscriber1 receives (Subscriber2 expired)
}

Output: 다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

[Subscriber1] Event received: First announcement
[Subscriber2] Event received: First announcement
Observer 'Subscriber2' destroyed
[Subscriber1] Event received: Second announcement

Explanation: Subject stores subscribers as weak_ptr, so when obs2 goes out of scope and destructs, Subject doesn’t keep it alive. During notify(), expired() entries are removed and only valid subscribers (via lock()) receive notifications.

Cache Pattern

Caches follow “use if available, create new or ignore if not” structure. Holding cached objects as weak_ptr enables automatic memory freeing when no longer used externally. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <unordered_map>
#include <string>
template<typename T>
class ResourceCache {
    std::unordered_map<std::string, std::weak_ptr<T>> cache;
public:
    std::shared_ptr<T> get(const std::string& key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            if (auto resource = it->second.lock()) {
                return resource;  // Cache hit
            }
            cache.erase(it);  // Remove expired entry
        }
        return nullptr;  // Cache miss
    }
    void put(const std::string& key, std::shared_ptr<T> resource) {
        cache[key] = resource;
    }
};

Complete cache pattern example (runnable): 다음은 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 freed: " << 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, loading: " << path << "\n";
        auto tex = std::make_shared<Texture>(path);
        cache_[path] = tex;
        return tex;
    }
};
int main() {
    TextureCache cache;
    {
        auto tex1 = cache.get("grass.png");   // Cache miss → load
        auto tex2 = cache.get("grass.png");   // Cache hit
    }  // tex1, tex2 freed → Texture "grass.png" destroyed
    auto tex3 = cache.get("grass.png");  // Only weak_ptr remains in cache → expired → reload
}

Output: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Cache miss, loading: grass.png
  Texture loaded: grass.png
Cache hit: grass.png
  Texture freed: grass.png
Cache miss, loading: grass.png
  Texture loaded: grass.png

Explanation: Cache stores textures as weak_ptr, so when tex1 and tex2 are freed, grass.png’s refcount hits 0, automatically freeing memory. Subsequent get("grass.png") finds only expired weak_ptr in cache, so it reloads. This enables automatic resource freeing when “not in use anywhere.”

6. Common Mistakes and Solutions

1. Use-after-free: Not checking lock() result

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ Dangerous: lock() can return empty shared_ptr
std::weak_ptr<Guild> wp = getGuildWeak();
wp.lock()->sendMessage("hello");  // UB if wp expired!
// ✅ Safe
if (auto g = wp.lock()) {
    g->sendMessage("hello");
}

2. Missing default handling on lock() failure

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ Logic error: doesn't handle empty shared_ptr on expiration
std::shared_ptr<Config> getConfig() {
    return configWeak_.lock();  // Empty shared_ptr on expiration
}
// Caller crashes without nullptr check
// ✅ Explicit handling
std::shared_ptr<Config> getConfig() {
    if (auto c = configWeak_.lock()) return c;
    return std::make_shared<Config>();  // Return default
}

3. Attempting direct weak_ptr to shared_ptr conversion

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ weak_ptr doesn't implicitly convert to shared_ptr
std::weak_ptr<int> wp = sp;
std::shared_ptr<int> sp2 = wp;  // Compile error
// ✅ Use lock()
std::shared_ptr<int> sp2 = wp.lock();

4. Wrong direction for breaking cycles

Only one side can be weak_ptr—choose the “non-owning side”. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ Correct choice
struct Character {
    std::weak_ptr<Guild> guild;  // Character doesn't own guild
};
struct Guild {
    std::vector<std::shared_ptr<Character>> members;  // Guild "manages" member list
};

5. Calling lock() on empty weak_ptr

If weak_ptr wasn’t created from any shared_ptr or already reset(), lock() returns empty shared_ptr. This is normal behavior, but problematic if caller assumes “always valid.” 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ Dangerous: wp might be empty weak_ptr
std::weak_ptr<Config> wp;  // Default construction → empty state
auto config = wp.lock();   // Returns empty shared_ptr
config->getValue();        // nullptr dereference → crash
// ✅ Safe: check lock() result
if (auto config = wp.lock()) {
    config->getValue();
}

6. Holding lock() result too long

Holding shared_ptr from lock() too long contradicts weak_ptr’s intent of “observe briefly and release.” Especially in observer/cache patterns, do necessary work and release quickly. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ⚠️ Caution: storing shared_ptr as member defeats weak_ptr purpose
class Handler {
    std::shared_ptr<Service> service_;  // Holding lock() result continuously
public:
    void init(std::weak_ptr<Service> wp) {
        service_ = wp.lock();  // Lock once and keep holding
    }
    // As long as service_ is alive, Service won't be freed → weak_ptr effect lost
};
// ✅ Recommended: lock() each time needed
void handle(std::weak_ptr<Service> wp) {
    if (auto s = wp.lock()) {
        s->doWork();  // After work, s scope ends → reference released
    }
}

7. Thread safety misconceptions

weak_ptr’s lock() and expired() are thread-safe, but using the object obtained from lock() across multiple threads requires separate synchronization. weak_ptr doesn’t provide a “thread-safe pointer.” 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ lock() being thread-safe doesn't make object access safe
std::weak_ptr<SharedData> wp = sharedData;
std::thread t1([wp]() { if (auto p = wp.lock()) p->modify(); });
std::thread t2([wp]() { if (auto p = wp.lock()) p->modify(); });
// SharedData::modify() itself needs synchronization like mutex

8. Cycles with 3+ objects

For A→B→C→A cycles with 3 or more objects, making just one weak_ptr breaks the cycle. No need to make all reverse directions weak. 다음은 간단한 cpp 코드 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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 in A→B→C→A cycle
// Cycle broken

9. weak_ptr copy vs reference passing

When capturing in lambdas, capture by value for safety beyond scope. Reference capture risks dangling references. 다음은 간단한 cpp 코드 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ Value capture
void registerCallback(std::weak_ptr<Service> wp) {
    queue.push([wp]() { if (auto s = wp.lock()) s->handle(); });
}

10. Creating weak_ptr from raw pointer

weak_ptr can only connect to objects managed by shared_ptr. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ Compile error: can't create weak_ptr from raw pointer
MyClass* raw = new MyClass();
std::weak_ptr<MyClass> wp(raw);
// ✅ Create from shared_ptr
auto sp = std::make_shared<MyClass>();
std::weak_ptr<MyClass> wp = sp;

6.5 weak_ptr Best Practices

Principle 1: Always check lock() result

Not checking shared_ptr returned by lock() before use risks use-after-free. Make if (auto p = wp.lock()) a habit.

Principle 2: expired() is auxiliary

In multithreaded code, objects can be freed between expired() check and lock(), so judge only by lock() result. Use expired() only for “rough filtering” or “statistics collection.”

Principle 3: Make non-owning side weak

Clarify “this object owns that object” relationships, making the non-owning side weak_ptr. Example: Character doesn’t own guild → Character::guild is weak_ptr.

Principle 4: Hold lock() result briefly

In observer/cache patterns, use shared_ptr from lock() only as local variable, not storing as member for long. This maintains weak_ptr’s “observe briefly and release” intent.

Principle 5: Clean up expired entries

Periodically removing expired() weak_ptr from observer lists and caches reduces memory and iteration costs.

Principle 6: Capture by value in lambdas

In async callbacks, capture weak_ptr by value for safety. Reference capture risks dangling references. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ Value capture
asyncTask([wp]() { if (auto w = wp.lock()) w->update(); });
// ❌ Reference capture: UB if wp destructs outside scope
asyncTask([&wp]() { if (auto w = wp.lock()) w->update(); });

Principle 7: Combine with optional for “absent” expression

When lock() returns empty shared_ptr, use std::optional to explicitly express “object absent.” 다음은 간단한 cpp 코드 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::optional<std::string> getGuildName(std::weak_ptr<Guild> wp) {
    if (auto g = wp.lock()) return g->name;
    return std::nullopt;
}

7. Performance Comparison: shared_ptr vs weak_ptr

Operation costs

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)
weak_ptr’s lock() internally increments ref_count, costing similar to shared_ptr copy. However, storing without incrementing reference count is lighter than shared_ptr, as it doesn’t affect object lifetime.

Memory usage

  • shared_ptr: object + control block (ref_count, weak_count, deleter, etc.)
  • weak_ptr: References control block only. Even after object freed, control block persists while weak_ptr remains. Summary: For “store and occasionally access” patterns (observer lists, caches), weak_ptr is suitable.

lock() call sequence

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

sequenceDiagram
    participant Caller
    participant weak_ptr
    participant ControlBlock
    participant Object
    Caller->>weak_ptr: lock()
    weak_ptr->>ControlBlock: Check ref_count
    alt ref_count > 0
        ControlBlock->>ControlBlock: ref_count++
        ControlBlock->>Object: Return shared_ptr
        weak_ptr->>Caller: shared_ptr (valid)
    else ref_count == 0
        weak_ptr->>Caller: Empty shared_ptr
    end

Explanation: lock() atomically checks control block’s ref_count. If 0, object already freed, so returns empty shared_ptr. If > 0, increments ref_count and returns valid shared_ptr.

8. Production Patterns: Event Systems & Resource Caches

Event System

Event publishers managing subscribers as weak_ptr remain safe even if subscribers destruct first. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
#include <functional>
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)) {
        std::weak_ptr<T> wp = subscriber;
        handlers.push_back({
            std::weak_ptr<void>(subscriber),
            [wp, method](int value) {
                if (auto s = wp.lock()) (s.get()->*method)(value);
            }
        });
    }
    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) {
            if (!h.target.expired()) h.callback(value);
        }
    }
};

Resource Cache (Textures, Models, etc.)

In game/rendering engines, caching resources as weak_ptr enables automatic memory freeing when “not in use anywhere.” Same pattern as the TextureCache complete example above. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class TextureCache {
    std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;
    std::shared_ptr<Texture> loader_(const std::string& path);
public:
    std::shared_ptr<Texture> get(const std::string& path) {
        if (auto it = cache_.find(path); it != cache_.end()) {
            if (auto tex = it->second.lock()) return tex;
            cache_.erase(it);
        }
        auto tex = loader_(path);
        cache_[path] = tex;
        return tex;
    }
};

Parent-Child Trees (DOM, AST, Config Trees)

For DOM nodes, abstract syntax trees (AST), config parser nodes where parent references child and child references parent, making child→parent weak_ptr breaks the cycle. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
struct TreeNode : std::enable_shared_from_this<TreeNode> {
    std::string name;
    std::weak_ptr<TreeNode> parent;  // Doesn't own parent
    std::vector<std::shared_ptr<TreeNode>> children;  // Owns children
    static std::shared_ptr<TreeNode> create(const std::string& n) {
        auto node = std::make_shared<TreeNode>();
        node->name = n;
        return node;
    }
    void addChild(std::shared_ptr<TreeNode> child) {
        child->parent = std::weak_ptr<TreeNode>(shared_from_this());
        children.push_back(std::move(child));
    }
};

Explanation: TreeNode holding parent as weak_ptr prevents children from keeping parents alive even after parent freed. Conversely, parent managing children as shared_ptr keeps children alive while parent exists. Freeing the root properly destructs the entire tree in order.

Callback/Handler Lifetime Management

In async callbacks, use weak_ptr captured in lambdas for “call if object still alive, ignore if not.” 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void asyncFetch(std::weak_ptr<Widget> widget) {
    fetchFromNetwork([widget](Response r) {
        if (auto w = widget.lock()) {
            w->onDataReceived(r);  // Update if widget still exists
        }
        // Safely ignore if widget already closed
    });
}

Plugin/Module Systems

Plugin managers storing loaded plugins as weak_ptr enable automatic expiration on plugin release. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void PluginManager::broadcast(const std::string& event) {
    plugins_.erase(
        std::remove_if(plugins_.begin(), plugins_.end(),
            [](auto& w) { return w.expired(); }),
        plugins_.end()
    );
    for (auto& w : plugins_) {
        if (auto p = w.lock()) p->onEvent(event);
    }
}

Network Sessions & Timer Callbacks

Server session lists or delayed execution callbacks storing target objects as weak_ptr enable automatic expiration on connection close or object deletion. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Session broadcast
void broadcast(const Message& msg) {
    for (auto& w : sessions_) {
        if (auto s = w.lock()) s->send(msg);
    }
}
// Timer callback: update if object still exists, ignore if not
void scheduleUpdate(std::weak_ptr<GameObject> obj, int delayMs) {
    timer.schedule(delayMs, [obj]() {
        if (auto o = obj.lock()) o->update();
    });
}

9. When to Use weak_ptr: Character and Guild

”Ownership” vs “Reference Only”

  • Ownership relationship: If this object disappears, that object is meaningless or should be cleaned up together → shared_ptr.
  • Reference only: “Use if available, ignore if not (already deleted)” → weak_ptr.

Example: Character and Guild in Games

  • Character needs to know its guild. If guild disbanded (deleted), character simply becomes “no guild.”
  • Guild has list of member characters. If character logs out or deleted, just remove from list. When guild → character and character → guild are both “reference only” not “ownership,” weak_ptr is suitable. Making guild members weak and character guild weak breaks cycles, and if one side deletes first, just handle expiration.
  • Character’s “my guild” → weak_ptr<Guild>: Use only when valid via lock().
  • Guild’s “member list” → weak_ptr<Character>: Iterate only living characters via lock(). Typical weak_ptr situations: “Mutual references where one side can die first, and shouldn’t be ‘owned’ by the relationship.”

10. Interview Answers

Q: Difference between lock() and expired()?

expired() only checks if object is freed. lock() returns shared_ptr if valid, empty shared_ptr if expired. In multithreaded environments, objects can be freed between expired() check and lock(), so judge only by lock() result for safety.”

Q: When do you use weak_ptr?

“Use to break circular references. When two objects point to each other with shared_ptr, refcount never hits 0, causing memory leaks. Making one side weak_ptr prevents count increase in that direction, breaking the cycle and enabling proper object deallocation. Also use for reference-only relationships where you ‘use if available, ignore if not.’ For example, in games, if a character holds guild as weak_ptr, the character won’t keep guild alive after disbanding, and can use it only when valid via lock().”

Q: What is circular reference? How to solve?

”When A holds B as shared_ptr and B holds A as shared_ptr, they mutually own each other, preventing refcount from reaching 0, causing memory leak. This is circular reference. Solution: make one side weak_ptr. Since weak_ptr doesn’t increment count, the cycle breaks, and you obtain shared_ptr via lock() only when needed.” This level—“circular reference → break one side with weak_ptr → practical example (character–guild)“—is sufficient.


Keywords

weak_ptr, circular reference, shared_ptr memory leak, lock expired, observer pattern, cache pattern

weak_ptr Application Checklist

  • Select non-owning side as weak_ptr
  • Check return value after lock() call (if (auto p = wp.lock()))
  • Never directly access expired weak_ptr (wp.lock()->foo() ❌)
  • Trust only lock() result in multithreaded environments
  • Clean up expired entries in observers/caches

Summary

  • Using shared_ptr only causes circular references (A→B→A) where refcount never hits 0, causing memory leaks.
  • weak_ptr doesn’t increment refcount, so making “one direction” weak breaks the cycle. Use lock() to obtain valid shared_ptr when needed.
  • weak_ptr use cases: Breaking circular references, and reference-only relationships (e.g., character–guild, observers, caches) where “use if available, expire if not.”
  • Trust only lock() results, using expired() only as auxiliary.
  • In production, use weak_ptr in event systems, resource caches, etc. for safe lifetime management.

Practical Checklist

Before writing code

  • Is this technique the best solution for the current problem?
  • Can team members understand and maintain this code?
  • Does it meet performance requirements?

While writing code

  • Have all compiler warnings been resolved?
  • Have edge cases been considered?
  • Is error handling appropriate?

During code review

  • Is the code’s intent clear?
  • Are test cases sufficient?
  • Is it documented? Use this checklist to reduce mistakes and improve code quality.

Frequently Asked Questions (FAQ)

Q. When do I use this in production?

A. Use weak_ptr in game servers (character–guild), GUI event subscriptions, resource caches, observer patterns. If circular references suspected, try making one side weak_ptr.

Q. Difference between weak_ptr and raw pointer?

A. Raw pointers can’t tell “if pointed object is freed,” causing undefined behavior (UB) on dereference. weak_ptr can check expiration with expired() and safely obtain shared_ptr via lock(), eliminating dangling pointer risk.

Q. What about multiple circular references?

A. Even for A→B→C→A cycles with 3+ objects, making just one weak_ptr breaks the cycle. No need to make all reverse directions weak.

Q. What’s the overhead of weak_ptr?

A. lock() calls require atomic operations, costing similar to shared_ptr copy. However, for “store and occasionally access” patterns, it’s often better overall than holding shared_ptr since memory frees properly.

Q. What should I read first?

A. Check C++ Series Index for complete flow.

Q. How to study deeper?

A. Refer to cppreference and official library documentation. One-line summary: Break circular references with weak_ptr to handle structures unsolvable with shared_ptr alone. Next, read Data Race·Mutex·Atomic (#34-1). Previous: C++ Interview #33-2: Shallow/Deep Copy & Move Semantics Next: C++ shared_ptr Circular Reference Mastery (Parent-Child, Observer, Graph, Cache)

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