C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
49: Smart Pointers: weak_ptr
We've spent a lot of time talking about std::shared_ptr, and it feels like the "silver bullet" for memory management, right? If multiple things own an object, just share it. But there's a specific scenario that'll bite you every single time if you aren't careful. I call it the "death grip."
The Circular Trap
Let's look at a common real-world pattern: a parent-child relationship. Imagine we're building a simple organizational chart. An Employee has a manager, and a manager has a list of subordinates. It feels natural to use shared_ptr for both directions so that neither side gets deleted while the other still needs it.
#include <iostream>
#include <memory>
#include <vector>
#include <string>
struct Employee {
std::string name;
std::shared_ptr<Employee> manager;
std::vector<std::shared_ptr<Employee>> subordinates;
Employee(std::string n) : name(n) { std::cout < name < " created\n"; }
~Employee() { std::cout < name < " destroyed\n"; }
};
int main() {
{
auto boss = std::make_shared<Employee>("The Big Boss");
auto worker = std::make_shared<Employee>("The Hard Worker");
boss->subordinates.push_back(worker);
worker->manager = boss;
} // Everything should be cleaned up here
std::cout < "End of scope reached\n";
return 0;
}
I'm expecting to see "The Big Boss destroyed" and "The Hard Worker destroyed" before the "End of scope" message. But when I run this... nothing. The program just prints "End of scope reached" and exits. That's a memory leak, plain and simple.
Tracing the Leak
Why is this happening? Let's trace the reference counts in my head. When I create boss, the count is 1. When I create worker, their count is 1. Then, I add worker to the boss's list (count becomes 2) and I assign boss as the worker's manager (count becomes 2).
When we hit the closing brace, the local boss and worker variables are destroyed. This drops both counts from 2 down to 1. But here is the problem: the boss object still holds a pointer to the worker, and the worker object still holds a pointer to the boss. They are keeping each other alive in a suicide pact. Neither count can ever reach zero, so the destructors never run.
Breaking the Cycle
To fix this, we need a way to say, "I want to know about this object, but I don't want to own it." This is exactly what std::weak_ptr is for. A weak_ptr points to an object managed by shared_ptr, but it doesn't increase the reference count.
Let's try changing the manager pointer to a weak_ptr. In a corporate hierarchy, the manager "owns" the employee, but the employee just "references" the manager.
struct Employee {
std::string name;
std::weak_ptr<Employee> manager; // Changed from shared_ptr
std::vector<std::shared_ptr<Employee>> subordinates;
Employee(std::string n) : name(n) { std::cout < name < " created\n"; }
~Employee() { std::cout < name < " destroyed\n"; }
};
Now, when the local boss variable goes out of scope, the only thing pointing to it is the worker's weak_ptr. Since weak_ptr doesn't contribute to the reference count, the boss's count drops to 0. The boss is destroyed, which in turn destroys the subordinates vector, which drops the worker's count to 0. Everything cleans up perfectly.
The Cost of Weakness
There is a catch, though. You can't just use a weak_ptr like a regular pointer. You can't use -> or * on it. Why? Because the object it points to could have been deleted at any moment. It's "weak" for a reason.
To actually use the object, you have to "promote" it back to a shared_ptr using the lock() method. This tells C++, "I need to use this object right now; please make sure it doesn't vanish while I'm working with it."
void printManager(std::weak_ptr<Employee> weakMgr) {
// Try to convert weak_ptr to shared_ptr
if (auto sharedMgr = weakMgr.lock()) {
std::cout < "Manager is: " < sharedMgr->name < "\n";
} else {
std::cout < "Manager is gone!\n";
}
}
I've found that lock() is the most important part of this pattern. If you try to bypass it or use unsafe casts, you're right back to the land of segmentation faults. Always check if the shared_ptr returned by lock() is valid before using it.
📋 Practical Task
Fixing the Game Engine Component Dependency Leak
You are working on a game engine where GameObjects can have a target they are tracking (like a missile tracking a player) and a list of children (like a sword attached to a character's hand). Currently, the engine is leaking memory because GameObjects are using std::shared_ptr for both the target and the children, creating circular dependencies when a child object targets its parent.
Your Task:
- Modify the provided
GameObjectclass to usestd::weak_ptrfor thetargetmember to break potential cycles. - Implement a method called
printTargetName()that safely attempts to lock thetargetand print its name. If the target no longer exists, it should print "Target Lost". - Verify that when a
GameObjectis deleted, its destructor runs even if it was being targeted by another object.
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class GameObject {
public:
std::string name;
// TODO: Change this to break the cycle
std::shared_ptr<GameObject> target;
std::vector<std::shared_ptr<GameObject>> children;
GameObject(std::string n) : name(n) {}
~GameObject() { std::cout < "Destroying " < name < "\n"; }
// TODO: Implement this method safely
void printTargetName() {
// Your code here
}
};
int main() {
{
auto player = std::make_shared<GameObject>("Player");
auto missile = std::make_shared<GameObject>("Missile");
player->children.push_back(missile);
missile->target = player; // This creates the cycle!
missile->printTargetName();
}
std::cout < "Scope exited\n";
return 0;
}There are no comments for now.