본문으로 건너뛰기
Previous
Next
C++ Smart Pointers & Breaking Circular References with

C++ Smart Pointers & Breaking Circular References with

C++ Smart Pointers & Breaking Circular References with

이 글의 핵심

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.

// ❌ 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

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

#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)

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):

// ❌ 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):

// ✅ 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:
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.

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.

#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:

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.

#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.

// 실행 예제
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.”

// ❌ 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().

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

#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:

[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.

#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):

#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:

[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.

#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):

#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:

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

// ❌ 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

// ❌ 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

// ❌ 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”.

// ✅ 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.”

// ❌ 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.

// ⚠️ 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.”

// ❌ 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.

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.

// ✅ 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.

// ❌ 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.

// ✅ 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.”

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

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.

#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.

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.

#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.”

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.

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.

// 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)


자주 묻는 질문 (FAQ)

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

A. Complete guide to solving circular reference memory leaks with weak_ptr. Learn lock(), expired(), observer pattern, cach… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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


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

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


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

C++, interview, smart pointers, shared_ptr, weak_ptr, circular reference, memory leak, observer pattern 등으로 검색하시면 이 글이 도움이 됩니다.