DEV Community

Rui-Tech
Rui-Tech

Posted on

Beyond new and delete: to Weak Pointer

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 ✅
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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! ✅
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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)
};
Enter fullscreen mode Exit fullscreen mode

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(...));
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
    }
};
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?

Top comments (0)