[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
- shared_ptr and Reference Count Review
- Circular References and Memory Leaks
- Breaking Cycles with weak_ptr
- lock() and expired() Detailed Examples
- weak_ptr Usage Patterns: Observer & Cache
- Common Mistakes and Solutions 6.5. weak_ptr Best Practices
- Performance Comparison: shared_ptr vs weak_ptr
- Production Patterns: Event Systems & Resource Caches
- When to Use weak_ptr: Character and Guild
- 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
| Operation | shared_ptr | weak_ptr |
|---|---|---|
| Copy | atomic ref_count++ | Control block access only |
| Destruction | atomic ref_count— | atomic weak_count— |
| lock() | — | atomic ref_count++, create shared_ptr |
| expired() | — | Check ref_count == 0 (atomic) |
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 vialock(). - Guild’s “member list” →
weak_ptr<Character>: Iterate only living characters vialock(). 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.
Related Articles
- C++ shared_ptr Circular Reference Mastery | Parent-Child, Observer, Graph, Cache Patterns [#33-4]
- C++ Smart Pointers | unique_ptr/shared_ptr “Memory Safety” Guide
- C++ Smart Pointers | Solving 3-Day Circular Reference Bug
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)