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. class Person { std::shared_ptr mother; // Owning reference }; auto alice = std::make_shared(); auto bob = std::make_shared(); // alice's reference count = 1 // bob's reference count = 1 // When they go out of scope, both are destroyed ✅ class Person { std::shared_ptr mother; std::shared_ptr father; }; auto alice = std::make_shared(); // alice ref count = 1 auto bob = std::make_shared(); // 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 💥 weak_ptr weak_ptr is like a "peek" at the object — it doesn't increase the reference count. class Person { std::shared_ptr mother; // Owning (increases count) std::weak_ptr father; // Observing (doesn't increase count) }; auto alice = std::make_shared(); // alice count = 1 auto bob = std::make_shared(); // 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! ✅ 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 > children; // Owning std::weak_ptr parent; // Observing (back-pointer) }; class Button { std::vector> observers; // Non-owning void onClick() { for (auto& weakObs : observers) { if (auto obs = weakObs.lock()) { obs->notify(); } } // Clean up dead observers observers.erase(remove_if(...)); } }; class ImageCache { std::map> cache; std::shared_ptr 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(path); cache[path] = img; // Store as weak_ptr return img; } }; shared_ptr.get() -> weak_ptr The comment about .get() means: If you ever write this: // BAD pattern std::shared_ptr sp = std::make_shared(); 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 sp = std::make_shared(); std::weak_ptr wp = sp; // Non-owning reference // Later: wp.lock() to safely access 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?
