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
- 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
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
| 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
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 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)
자주 묻는 질문 (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++ shared_ptr 순환 참조 완전 정복 | 부모-자식·옵저버·그래프·캐시 패턴 [#33-4]
- C++ 스마트 포인터 | 3일 동안 찾지 못한 순환 참조 버그 해결법
- C++ weak_ptr | ‘약한 포인터’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, interview, smart pointers, shared_ptr, weak_ptr, circular reference, memory leak, observer pattern 등으로 검색하시면 이 글이 도움이 됩니다.