In the previous article, we left one case untouched: the transition from raw pointers to weak_ptr. That's exactly what we'll dive into today.
Use shared_ptr when multiple parts of your system need to keep an object alive, and you can't predict which part will outlive the others. But shared_ptr has a fatal flaw: cyclic references.
When two shared_ptrs point to each other, neither can die. They hold each other hostage forever. The result? A memory leak that never gets cleaned up.
Let me break it down step by step.
The Core Problem: What happens when objects point to each other?
Normal case (no cycle)
class Person {
std::shared_ptr<Person> mother; // Owning reference
};
auto alice = std::make_shared<Person>();
auto bob = std::make_shared<Person>();
// alice's reference count = 1
// bob's reference count = 1
// When they go out of scope, both are destroyed ✅
The cyclic problem
class Person {
std::shared_ptr<Person> mother;
std::shared_ptr<Person> father;
};
auto alice = std::make_shared<Person>(); // alice ref count = 1
auto bob = std::make_shared<Person>(); // bob ref count = 1
alice->father = bob; // bob's ref count becomes 2
bob->mother = alice; // alice's ref count becomes 2
Now what happens when alice and bob go out of scope?
-
alice(the variable) is destroyed → alice's ref count drops from 2 → 1 -
bob(the variable) is destroyed → bob's ref count drops from 2 → 1 - Both objects still have ref count = 1! They point to each other, so neither can be destroyed.
- MEMORY LEAK 💥
The Solution: weak_ptr
weak_ptr is like a "peek" at the object — it doesn't increase the reference count.
class Person {
std::shared_ptr<Person> mother; // Owning (increases count)
std::weak_ptr<Person> father; // Observing (doesn't increase count)
};
auto alice = std::make_shared<Person>(); // alice count = 1
auto bob = std::make_shared<Person>(); // bob count = 1
alice->father = bob; // bob's count stays 1 (weak_ptr doesn't affect it)
bob->mother = alice; // alice's count becomes 2
// When variables go out of scope:
// bob count: 1 → 0 (destroyed)
// alice count: 2 → 1 → 0 (destroyed when bob's weak_ptr expires)
// NO LEAK! ✅
Using weak_ptr: The .lock() method
You can't use a weak_ptr directly — you must first "lock" it to get a temporary shared_ptr. .lock() atomically checks existence and acquires a shared_ptr in one thread-safe operation — there is no other safe way to access a weak_ptr's target.
// BAD: Can't use weak_ptr directly
father->doSomething(); // Compiler error!
// GOOD: Lock it first
if (auto temp = father.lock()) { // Try to get shared_ptr
temp->doSomething(); // Use it safely
} else {
// The object has been destroyed
std::cout << "Father is gone\n";
}
Real-World Examples
1. Parent-child relationships
class Node {
std::vector<std::shared_ptr<Node>> children; // Owning
std::weak_ptr<Node> parent; // Observing (back-pointer)
};
2. Event observers
class Button {
std::vector<std::weak_ptr<ClickObserver>> observers; // Non-owning
void onClick() {
for (auto& weakObs : observers) {
if (auto obs = weakObs.lock()) {
obs->notify();
}
}
// Clean up dead observers
observers.erase(remove_if(...));
}
};
3. Caches
class ImageCache {
std::map<std::string, std::weak_ptr<Image>> cache;
std::shared_ptr<Image> get(const std::string& path) {
auto it = cache.find(path);
if (it != cache.end()) {
if (auto img = it->second.lock()) {
return img; // Still in use, return it
}
}
// Not in cache or expired, load new image
auto img = std::make_shared<Image>(path);
cache[path] = img; // Store as weak_ptr
return img;
}
};
The shared_ptr.get() -> weak_ptr
The comment about .get() means: If you ever write this:
// BAD pattern
std::shared_ptr<Widget> sp = std::make_shared<Widget>();
Widget* raw = sp.get(); // Storing raw pointer
// Later: use raw somewhere else — DANGEROUS!
That's usually a sign you should use weak_ptr instead:
// GOOD pattern
std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp; // Non-owning reference
// Later: wp.lock() to safely access
Key Takeaways
shared_ptr |
weak_ptr |
|---|---|
| Owns the object | Observes the object |
| Increases ref count | Doesn't affect ref count |
| Keeps object alive | Object can die |
| Always valid (until destroyed) | May be expired |
Direct access with ->
|
Must call .lock() first |
When to use weak_ptr: Any time you need a reference to an object but don't want to be responsible for keeping it alive — especially parent back-pointers, observers, and caches.
Does this make the cyclic reference problem clearer?











