VOOZH about

URL: https://dev.to/legacycpp/beyond-new-and-delete-to-weak-pointer-10l6

⇱ Beyond new and delete: to Weak Pointer - DEV Community


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?