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.

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.

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.

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.

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.

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.

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_ptrThe Default. Exclusive ownership.
shared_ptrTrue shared ownership.
weak_ptrBreak 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.

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.