Hello! Welcome back to our module on modern C++ resource management.
In our last lesson, we mastered std::unique_ptr, the tool for managing resources with a single, clear owner. We saw how its strict, exclusive ownership model, enforced by the compiler, eliminates many common memory management bugs.
However, some designs require a more flexible ownership model. What if multiple, independent parts of your program need to share a resource and keep it alive as long as any of them are still using it? This is the concept of shared ownership.
Today, you will learn to use std::shared_ptr for shared ownership of resources. This smart pointer is a cornerstone of modern C++ for managing complex object lifetimes in situations like data caching, observer patterns, or shared data in concurrent applications.
1. From Exclusive to Shared Ownership
While std::unique_ptr models a one-to-one relationship with a resource, std::shared_ptr models a many-to-one relationship. Multiple shared_ptr instances can co-own a single object. The C++ runtime guarantees the object will be destroyed automatically, but only when the very last shared_ptr pointing to it is gone.
How does it achieve this? Through a mechanism called reference counting.
Let's start with a video that introduces this idea and contrasts it with the pointers you already know.
std::shared_ptr - A reference counted smart pointer | Modern Cpp Series Ep. 34
Mike Shah provides a clear introduction to the concept of shared ownership and why it's a powerful idea that complements the exclusive ownership of unique_ptr.
Watch from the beginning to 01:56. This will set the stage for why we need shared pointers and what problem they solve.
2. Under the Hood: The Control Block and Reference Counting
As you've expressed interest in how things work internally, std::shared_ptr is a fascinating case study. A shared_ptr is slightly more complex than a raw pointer or unique_ptr. Internally, when you create the first shared_ptr for a resource, C++ allocates a small, separate block of memory called the control block.
This control block contains:
- A pointer to the managed object.
- A reference count: The number of
shared_ptrinstances that currently co-own the object. - A "weak count" (for
std::weak_ptr, which we will touch on later). - Other data, like a custom deleter if one was provided.
Every shared_ptr that co-owns the same object holds two pointers: one to the object itself, and one to this shared control block.

When a shared_ptr is copied, the reference count in the control block is atomically incremented. When a shared_ptr is destroyed or reset, the reference count is decremented. If the count hits zero, the control block deletes the managed object and then deletes itself.
The following video segment explains this mechanism visually.
std::shared_ptr - A reference counted smart pointer | Modern Cpp Series Ep. 34
Let's return to Mike Shah's video, where he draws out the relationship between shared pointers, the object, and the control block.
Watch from 04:19 to 07:38. Focus on how the reference count acts as the trigger for deallocation.
3. Creating and Using std::shared_ptr
Like unique_ptr, shared_ptr is defined in the <memory> header. The best practice for creating one is using std::make_shared.
std::make_shared: The Preferred Way
#include <iostream>
#include <memory> // Required for std::shared_ptr and std::make_shared
struct Project {
std::string name;
Project(const std::string& n) : name(n) { std::cout << "Project '" << name << "' created.\n"; }
~Project() { std::cout << "Project '" << name << "' destroyed.\n"; }
};
int main() {
// Create a shared_ptr that owns a new Project object.
auto sp1 = std::make_shared<Project>("Music Notation App");
}
// `sp1` goes out of scope. Ref count becomes 0. Project is destroyed.
Why make_shared is even more important here:
Using new (e.g., std::shared_ptr<Project> p(new Project(...));) forces two separate dynamic allocations: one for the Project object and another for the control block. std::make_shared is more efficient because it performs a single, larger allocation that holds space for both the object and the control block, reducing allocation overhead.
This video from The Cherno explains this performance advantage clearly.
SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)
The Cherno explains why make_shared is more efficient than using new directly with shared_ptr.
Watch from 06:20 to 07:25. He contrasts new with make_shared and explains the allocation optimization.
Sharing and Observing use_count()
The magic of shared_ptr happens when you copy it. This creates another owner, and the reference count goes up. You can inspect the current reference count with the .use_count() method, which is very useful for learning and debugging.
Let's see this in action.
#include <iostream>
#include <memory>
#include <string>
struct Project {
std::string name;
Project(const std::string& n) : name(n) { std::cout << " [ctor] Project '" << name << "'\n"; }
~Project() { std::cout << " [dtor] Project '" << name << "'\n"; }
};
int main() {
std::shared_ptr<Project> sp1; // sp1 is created empty.
std::cout << "sp1 created. use_count: " << sp1.use_count() << "\n\n";
{
auto sp2 = std::make_shared<Project>("Music App");
std::cout << "sp2 created. use_count: " << sp2.use_count() << "\n";
// Share ownership with sp1
sp1 = sp2;
std::cout << "sp1 shares ownership. sp1 use_count: " << sp1.use_count()
<< ", sp2 use_count: " << sp2.use_count() << "\n";
auto sp3 = sp1; // Create a third owner
std::cout << "sp3 created from sp1. use_count: " << sp1.use_count() << "\n";
std::cout << "\nInner scope is about to end...\n";
} // sp2 and sp3 go out of scope. Two owners are gone.
std::cout << "\nInner scope ended. sp1 use_count: " << sp1.use_count() << "\n";
std::cout << "Main function is about to end...\n";
} // sp1 goes out of scope. The last owner is gone. The Project is destroyed.
If you compile and run this code, you'll see the reference count change and can track exactly when the Project object is created and destroyed. The lifecycle is visualized perfectly in the image below.

4. Guidelines for Passing shared_ptr
How you pass a shared_ptr to a function signals your intent regarding ownership.
-
Pass by value (
void func(std::shared_ptr<T> sp)):- Meaning: The function
funcwants to become a co-owner of the object. - Effect: The reference count is incremented. The function can store its copy, extending the object's lifetime beyond the caller's scope.
- When: Use this when the function needs to store the pointer for later use (e.g., adding it to a list of observers, scheduling it for a background task).
- Meaning: The function
-
Pass by
constreference (void func(const std::shared_ptr<T>& sp)):- Meaning: The function
funconly wants to access or "borrow" the object, without affecting its lifetime. - Effect: The reference count does not change. This is more efficient as it avoids a copy and atomic increment.
- When: This is the most common case. Use it when the function will only use the object for the duration of the call.
- Meaning: The function
This topic is crucial for writing clean and efficient C++. The following reading provides an excellent, detailed guide.
How to: Create and use shared_ptr instances
The Microsoft C++ documentation provides a definitive guide on how and when to pass shared_ptr to functions. This is a must-read for understanding correct usage patterns.
Read the section titled 'Example 5', which discusses the different ways to pass a shared_ptr to a function. Pay close attention to the bullet points that explain the trade-offs of passing by value versus passing by reference.
5. A Critical Pitfall: Cyclic References
shared_ptr is powerful, but it has one major weakness: cyclic references.
Imagine you have two objects, A and B.
- Object A holds a
shared_ptrto object B. - Object B holds a
shared_ptrback to object A.
struct B; // Forward declaration
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
struct B {
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A owns B
b->a_ptr = a; // B owns A
} // a and b go out of scope.
When main ends, the shared_ptr a is destroyed. This decrements the reference count for the A object from 2 to 1 (because b->a_ptr still exists). The shared_ptr b is also destroyed, decrementing the reference count for the B object to 1. Since neither count reached zero, neither destructor is ever called. This is a classic memory leak.
The Solution: The solution to this problem is std::weak_ptr. A weak_ptr is a non-owning smart pointer that can observe an object managed by a shared_ptr without increasing the reference count. This breaks the cycle. While a full exploration of weak_ptr is beyond this lesson, it's critical to be aware of this issue and its solution. Case studies in the document Smart Pointers in Modern CPP.pdf (sections 10.2 and 10.3) show this pattern used in game development and the observer pattern.
Conclusion
You now have the second major tool for modern C++ resource management. While unique_ptr should be your default choice for its simplicity and efficiency, shared_ptr provides the necessary flexibility for more complex ownership scenarios.
Key Takeaways:
std::shared_ptrmanages resources via shared ownership and reference counting.- The resource is deallocated only when the last
shared_ptrco-owner is destroyed. - Always prefer
std::make_sharedto createshared_ptrs for better performance and exception safety. - Passing by
constreference is for non-owning access (borrowing), while passing by value is for extending ownership. - Beware of the performance overhead (compared to
unique_ptr) and the critical pitfall of cyclic references, which lead to memory leaks.
Next Up
We've now seen std::move used to transfer ownership of a std::unique_ptr. This function is a key piece of modern C++ that enables high-performance operations by avoiding unnecessary copies. In our next lesson, we will pull back the curtain and dive deep into the concepts that make it possible: move semantics and rvalue references.