Shared Ownership and Weak Pointers
Introduction to Shared Ownership
Beyond Exclusive Ownership
While std::unique_ptr is the default for exclusive ownership, real-world applications often need multiple components to access the same data. Enter std::shared_ptr.
Unlike unique pointers, multiple shared pointers can point to the same resource. The resource is only destroyed when the last owner is gone.
Welcome to the final lesson on C++ memory management. Today we explore shared ownership. Imagine a single resource, like a database connection, that one part of your app needs. But then, another part needs it too. With shared pointers, both can own it simultaneously. When the first shared pointer is created, the reference count becomes one. When the second pointer is assigned to the same resource, the count increments to two. The resource stays alive as long as either pointer exists.
- std::shared_ptr allows multiple owners of a single resource.
- Resource lifetime is managed automatically via reference counting.
- The object is deleted only when the count reaches zero.
The Control Block and make_shared
How it Works: The Control Block
A std::shared_ptr isn't just a pointer; it's a small object that manages a control block. This block stores the reference count and other metadata.
Best Practice: Always use std::make_shared<T>(). It allocates the object and the control block in a single memory chunk, which is faster and safer.
Internally, a shared pointer manages two things: the object itself and a control block. The control block is where the reference count lives. If you use 'new', C++ has to make two separate memory allocations. But by using std::make_shared, both are allocated together in one efficient block of memory.
- The control block tracks the number of shared_ptr instances.
- std::make_shared is more efficient than using 'new'.
- Single allocation improves cache locality and exception safety.
Reference Counting in Action
Interact with the Counter
Click the buttons to create or destroy shared_ptr instances and watch the reference count in the control block.
Notice how the count increments. Every new owner keeps the resource alive. Let's see the reference count in action. Try creating new shared pointers and then destroying them to see when the memory is actually freed. As pointers go out of scope, the count drops. If it hits zero, the resource is deleted.
- Copying a shared_ptr increments the count.
- Destroying a shared_ptr (going out of scope) decrements the count.
- Memory is freed immediately when count hits 0.
The Problem: Cyclic References
The 'Deadly Embrace'
A cyclic reference occurs when two objects hold shared_ptr to each other. Because they both keep each other's reference count at 1 (or higher), they can never be deleted.
This creates a permanent memory leak, even if the rest of your program no longer has access to them.
Here is a dangerous scenario. Object A owns Object B, and Object B owns Object A. Even if our main program deletes its pointers to them, their internal counts stay at 1. They are stuck in memory forever—a classic memory leak.
- Cycles prevent the reference count from ever reaching zero.
- Standard shared_ptrs cannot detect or break these cycles automatically.
- This is a common source of leaks in complex data structures like graphs.
The Solution: std::weak_ptr
Breaking the Cycle
A std::weak_ptr is a non-owning observer. It points to a resource managed by a shared_ptr but does not increment the reference count.
To use the data, you must call .lock(), which temporarily converts the weak pointer into a shared pointer (if the object still exists).
When we need to use the data, we call .lock(). This gives us a temporary shared_ptr, but only if the object is still there. To fix the cycle, we change one of the pointers to a weak pointer. Look! The reference count doesn't go up. Now, when the main program lets go, the counts can finally hit zero, and the memory is reclaimed.
- weak_ptr allows you to 'peek' without taking ownership.
- It breaks cycles by keeping the reference count low.
- Always check if .lock() returns a valid pointer before use.
Practical Scenario: Folder & Files
In a file system, a Folder owns its Files. But sometimes a File needs to know which Folder it belongs to.
Assign the correct pointer types to prevent a memory leak.
Perfect! By using a shared pointer for the children and a weak pointer for the parent backlink, you've created a safe, leak-free structure. Here we have a Folder and several Files. Drag the correct pointer type to the relationship lines to ensure the Folder owns the Files, but the Files don't cause a leak when looking back at their Parent. Careful! If both are shared, you'll create a cycle. The Folder and File will never be deleted from memory.
- Parent-to-child should be shared (ownership).
- Child-to-parent should be weak (observation).
- This architecture ensures clean cleanup.
Decision Guide: Which Pointer to Use?
C++14/20 Pointer Hierarchy
Choosing the right pointer is critical for performance and safety. Follow this standard hierarchy:
| unique_ptr | The Default. Exclusive ownership. |
| shared_ptr | True shared ownership. |
| weak_ptr | Break cycles or for caches. |
| Raw (*) | Non-owning, nullable, no overhead. |
To wrap up the course, let's look at the decision hierarchy. Always start with unique_ptr—it has zero overhead. Move to shared_ptr only when multiple parts of the system share a lifetime. Use weak_ptr for cycles. And finally, raw pointers are still okay for simple, non-owning access where you don't need the smart pointer machinery.
- Always start with unique_ptr.
- Only upgrade to shared_ptr if ownership is truly shared.
- Use weak_ptr for back-pointers in data structures.
- Raw pointers are still useful for simple, non-owning observation.
Final Debugging Exercise
Examine the code snippet. It describes a Circular Linked List using only std::shared_ptr. Explain why this causes a leak and how to fix it.
Take a look at this code. It's a simple Node structure for a circular list. Type your diagnosis and a solution in the box below.
- Identify the cycle.
- Propose weak_ptr as the solution.
- Explain the benefit of .lock().