A std::shared_ptr is a smart pointer that allows sharing ownership of the same object among multiple parts of a program.
Now let’s dive deeper into one of the most flexible, yet often misunderstood types: the std::shared_ptr.
While std::unique_ptr owns a resource exclusively, std::shared_ptr allows multiple pointers to share its ownership.
Its philosophy is shared ownership. Multiple pointers can point to the same object, and the object is only destroyed when all the pointers have disappeared.
How Reference Counting Works
std::shared_ptr uses a technique called reference counting.
Internally, a shared_ptr not only stores the memory address of the object. It also maintains a pointer to a shared control block. This block contains a counter (an integer) that indicates how many shared_ptr instances are currently pointing to that object.
The logic is as follows:
When you create a shared_ptr, the counter starts at 1.
If you copy that shared_ptr (pass it to another function or to another object), the counter increases.
When a shared_ptr is destroyed (goes out of scope), the counter decreases.
If the counter reaches 0, it means no one needs the object anymore. The memory is freed automatically.
Operations on the control block are synchronized. Different shared_ptr instances sharing that block can be copied and destroyed from multiple threads, although this does not make concurrent access to the managed object safe and introduces a small overhead.
Creating a shared_ptr
To use it, we need to include the <memory> header.
Although we can construct it from new, the recommended and usual way is to use std::make_shared.
#include <memory>
#include <iostream>
int main() {
// CORRECT WAY: Using std::make_shared
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// NOT RECOMMENDED WAY (but possible)
// std::shared_ptr<int> ptr2(new int(10));
}
Why use std::make_shared?
There are two important technical reasons to prefer make_shared:
- Efficiency: If you use
new, you make two memory allocations (one for the object and one for the control block).make_sharedmakes them in a single contiguous memory block. - Safety: It avoids subtle memory leaks if an exception occurs right at the moment of argument creation.
Usage Example and Lifecycle
Let’s see a practical example to visualize how the reference counter changes using the .use_count() method.
#include <iostream>
#include <memory>
class Message {
public:
Message() { std::cout << "Message created\n"; }
~Message() { std::cout << "Message destroyed\n"; }
};
int main() {
std::cout << "Program start\n";
// 1. Create the first pointer
std::shared_ptr<Message> p1 = std::make_shared<Message>();
std::cout << "Counter p1: " << p1.use_count() << "\n"; // Output: 1
{
// 2. Create a second pointer COPYING p1
// Both point to the same thing.
std::shared_ptr<Message> p2 = p1;
std::cout << "Counter p1: " << p1.use_count() << "\n"; // Output: 2
std::cout << "Counter p2: " << p2.use_count() << "\n"; // Output: 2
} // 3. Here p2 goes out of scope and is destroyed. The counter decreases.
std::cout << "Counter p1 after closing block: " << p1.use_count() << "\n"; // Output: 1
std::cout << "Program end\n";
} // 4. Here p1 goes out of scope. Counter = 0. The destructor is called.
Program output:
Program start
Message created
Counter p1: 1
Counter p1: 2
Counter p2: 2
Counter p1 after closing block: 1
Program end
Message destroyed
As we can see, the Message object outlives the inner block because p1 was still “holding” it.
When to Use shared_ptr?
Many novice C++ programmers make the mistake of using shared_ptr for everything because it resembles the Garbage Collectors of languages like C# or Java. Do not do this.
A shared_ptr is heavier than a raw pointer: it usually stores two pointers and also has the overhead of managing the control block.
Use it only when:
- Several objects actually need to keep a third object alive, and it’s not clear which one will die last.
- Examples: Complex data structures (graphs), cached resources (textures in a video game used by multiple models), or passing asynchronous data between threads.
If you can define a clear owner for the object, use std::unique_ptr. If you only need to access the object without owning it, use a raw pointer (observer) or a reference.
The Danger of Circular References
The typical problem with std::shared_ptr is circular references. If Object A has a shared_ptr to Object B, and Object B has a shared_ptr to Object A… they will never be destroyed.
Both will always have a counter of 1, waiting for each other. This creates a Memory Leak.
To solve this, C++ provides an observer that does not increase the ownership counter: std::weak_ptr.