In this article, we will discuss one of the most used Smart Pointers in C++: shared_ptr
.
Table of Contents
What is std::shared_ptr<>?
The std::shared_ptr
is a smart pointer class provided by C++11. Multiple std::shared_ptr
objects can point to same dynamically allocated memory. It ensures automatic deletion of the associated memory when there are no more shared_ptr
instances pointing to it, thereby preventing memory leaks and dangling pointers.
Why Do We Need shared_ptr?
Shared Ownership
shared_ptr
employs the concept of shared ownership, meaning multiple shared_ptr
instances can own the same dynamically allocated memory. Internally, it utilizes a reference counting mechanism to keep track of how many shared_ptr
objects are associated with the same dynamically allocated memory. Destruction of each shared_ptr
object decrements the reference count. When the reference count reaches zero, the undeline dynalically memory gets deleted.
Safe Resource Management
C++ does not have garbage collection, and manual memory management is error-prone and can lead to issues like memory leaks, dangling pointers, and double deletions. A Smart Pointer like shared_ptr
helps manage dynamic memory safely and automatically. By wrapping a dynamically allocated object within a shared_ptr
, you ensure that it will be properly deleted when it’s no longer needed.
Facilitate Polymorphic Behavior
Frequently Asked:
- What is shared_ptr in C++?
- Shared_ptr & Custom Deleter in Modern C++
- How not to use Smart Pointers in C++?
- Smart Pointer vs Raw Pointer in C++
Since shared_ptr can be used with inheritance hierarchies, it enables polymorphic behavior when handling collections of base class pointers that point to derived instances.
Simplify Complex Data Structures
shared_ptr is particularly useful in the construction of complex data structures, such as graphs or trees with nodes that are shared among multiple elements. It simplifies the memory management aspect, which otherwise could become quite intricate.
Thread Safety
The shared_ptr implementation in the standard library is thread-safe in terms of reference counting operations. Multiple threads can create and destroy their own shared_ptr instances pointing to the same object without additional synchronization for the reference count.
Exception Safety
shared_ptr
provides strong exception safety guarantees. If an exception is thrown, it can help ensure that no memory leaks occur, as shared_ptr will automatically clean up unless it is explicitly caught and handled.
Decoupling
It allows for better decoupling of components since one part of a program can use an object without needing to know about other parts that might also be using it.
When Not to Use shared_ptr
Despite its benefits, shared_ptr is not always the best choice. These are the scenarios, when we should avoid shared_ptr
Smart Pointer,
- Overhead: The control block that manages the reference count has a performance and memory overhead.
- Cycles:
shared_ptr
can lead to memory leaks if there are cycles of references. This problem can be mitigated by usingweak_ptr
. - Not Always Necessary: If an object has a single, clear owner, a unique_ptr may be a more appropriate choice, as it has less overhead and makes ownership semantics explicit.
Creating a shared_ptr Object
To use the shared_ptr, we first need to include following header file,
#include <memory>
Then we can create a shared_ptr
object and bind it to a raw pointer, like this,
std::shared_ptr<int> p1(new int());
The above line results in the allocation of two blocks of memory on the heap:
- For the integer: The memory needed to store the actual integer.
- For the reference count: The control block that contains the reference count and any other control data needed to manage shared ownership. Initially, this count will be 1.
How shared_ptr works?
- Acquisition: When a new
shared_ptr
is created and points to an object, the reference count associated with that object is increased by one. - Copy or Assignment: When a
shared_ptr
is copied or assigned, the reference count will again increase since another smart pointer now points to the same object. - Destruction: When a
shared_ptr
goes out of scope or is reset, it decreases the reference count of its associated object by one. - Deletion: If the reference count becomes zero, which means no
shared_ptr
objects are pointing to the object, the allocated memory is deallocated using thedelete
operator.
How to Check Reference Count of a shared_ptr Object
We can check the reference count using the use_count()
member function. Let’s see an example:
std::shared_ptr<int> p1(new int()); // Returns the number of shared_ptr objects managing the same raw pointer auto count = p1.use_count();
Here, we created a shared_ptr object and assigned a raw pointer to it, which points to dynamically allocated memory. Now, this Smart Pointer object is responsible for managing this memory. If someone creates a copy of this shared_ptr object, then the reference count in both the shared_ptr objects will be 2. Let’s see an example:
#include <iostream> #include <memory> int main() { std::shared_ptr<int> p1(new int()); std::shared_ptr<int> p2 = p1; std::cout << "Reference Count: " << p1.use_count() << std::endl; std::cout << "Reference Count: " << p2.use_count() << std::endl; return 0; }
Output:
Reference Count: 2 Reference Count: 2
Here, both the shared_ptr
objects p1
and p2
are pointing to the same dynamically allocated integer memory. Therefore, the reference count in both p1
and p2
was 2. When p2
goes out of scope, the destructor of the shared_ptr
object p2
will be invoked; it will decrement the reference count by 1 and check if the reference count is 0, then only delete the dynamically allocated memory. But as the reference count is still 1, it means some other shared_ptr
object is still pointing to that memory, so the dynamic memory will not be deleted.
Now, the reference count in the shared_ptr
object p1
also becomes 1.
When the p1
object goes out of scope, the destructor of the shared_ptr
object p1
will be invoked; it will decrement the reference count by 1, and now the reference count will become 0. It means no other shared_ptr
object is pointing to the same memory, so it will delete the dynamically allocated memory.
With smart pointers, you don’t need to take care of memory management; they will automatically delete the dynamically allocated memory when it’s no longer needed.
Using std::make_shared for creating shared_ptr object
Incorrect Way to Assign a Pointer to shared_ptr
:
// This will cause a compile-time error // Error: Cannot convert 'int*' to 'std::shared_ptr<int>' in assignment std::shared_ptr<int> p1 = new int();
The constructor of shared_ptr
that takes a raw pointer is explicit, and thus, we cannot assign a raw pointer to a shared_ptr
implicitly. The best practice to create a new shared_ptr
is by using std::make_shared
, as shown below:
std::shared_ptr<int> p1 = std::make_shared<int>();
std::make_shared
performs a single memory allocation for both the object and the control block used for reference counting, making it more efficient than using new
separately.
Detaching the Associated Raw Pointer from shared_ptr
To make a shared_ptr
relinquish control of its managed object, you can use the reset()
method.
reset()
Function Without Parameter:
std::shared_ptr<int> p1 = std::make_shared<int>(); p1.reset();
This decreases its reference count by 1, and if the reference count becomes 0, it deletes the managed object, basically it will delete the dynamically allocated object.
reset()
Function With Parameter:
p1.reset(new int(34));
In this case, the shared_ptr
will manage a new pointer, and its reference count will become 1.
Resetting Using nullptr
:
p1 = nullptr; // Equivalent to p1.reset();
Setting the shared_ptr
to nullptr
will decrease the reference count and delete the managed object if the count reaches zero.
Using shared_ptr as a Pseudo-Pointer
The shared_ptr
in C++ is designed to provide a degree of indirection similar to that of raw pointers, with the added benefit of automatic memory management. Just like a regular pointer, you can use a shared_ptr
to access the object it owns directly through common pointer operations.
Dereferencing a shared_ptr
You can dereference a shared_ptr
using the unary *
operator to access the object it points to, just as you would with a raw pointer. Let’s see an example,
#include <memory> #include <iostream> class Message { public: void getText() const { std::cout << "Some Random Text!! \n"; } }; int main() { std::shared_ptr<Message> msgPtr = std::make_shared<Message>(); // Dereference the shared_ptr to call a function on the Message object (*msgPtr).getText(); }
Output:
Some Random Text!!
In this example, (*msgPtr)
dereferences msgPtr
to get to the Message
object, and then .getText()
is called on the Message
object.
Accessing Members of the Pointed-to Object
The ->
operator allows you to directly access members (methods or variables) of the object that the shared_ptr
points to.
Here’s an example:
#include <memory> #include <iostream> class Message { public: void getText() const { std::cout << "Some Random Text!! \n"; } }; int main() { std::shared_ptr<Message> msgPtr = std::make_shared<Message>(); // Use the -> operator to access members of the Message object msgPtr->getText(); }
Output:
Some Random Text!!
In this case, msgPtr->getText()
is shorthand for (*msgPtr).getText()
, which is typically more convenient and commonly used in C++ code.
Comparison Operations with shared_ptr
shared_ptr
instances can be compared with other shared_ptr
instances, or with nullptr
, to determine equality (==
) or inequality (!=
). Two shared_ptr
instances are equal if they point to the same object or if both are null.
For instance:
std::shared_ptr<Message> ptr1 = std::make_shared<Message>(); std::shared_ptr<Message> ptr2 = ptr1; // ptr2 now shares ownership with ptr1 std::shared_ptr<Message> ptr3; if (ptr1 == ptr2) { std::cout << "ptr1 and ptr2 point to the same object.\n"; } if (ptr3 == nullptr) { std::cout << "ptr3 is uninitialized and holds no object.\n"; }
Complete Example of shared_ptr
A complete example explaining all the details of shared_ptr is as follows,
#include <iostream> #include <memory> // We need to include this for shared_ptr int main() { // Creating a shared_ptr through make_shared std::shared_ptr<int> p1 = std::make_shared<int>(); *p1 = 78; std::cout << "p1 = " << *p1 << std::endl; // Shows the reference count std::cout << "p1 Reference count = " << p1.use_count() << std::endl; // Second shared_ptr object will also point to same pointer internally // It will make the reference count to 2. std::shared_ptr<int> p2(p1); // Shows the reference count std::cout << "p2 Reference count = " << p2.use_count() << std::endl; std::cout << "p1 Reference count = " << p1.use_count() << std::endl; // Comparing smart pointers if (p1 == p2) { std::cout << "p1 and p2 are pointing to same pointer\n"; } std::cout << "Reset p1 " << std::endl; p1.reset(); // Reset the shared_ptr, in this case it will not point to any Pointer internally // hence its reference count will become 0. std::cout << "p1 Reference Count = " << p1.use_count() << std::endl; // Reset the shared_ptr, in this case it will point to a new Pointer internally // hence its reference count will become 1. p1.reset(new int(11)); std::cout << "p1 Reference Count = " << p1.use_count() << std::endl; // Assigning nullptr will de-attach the associated pointer and make it to point null p1 = nullptr; std::cout << "p1 Reference Count = " << p1.use_count() << std::endl; if (!p1) { std::cout << "p1 is NULL" << std::endl; } return 0; }
Output:
p1 = 78 p1 Reference count = 1 p2 Reference count = 2 p1 Reference count = 2 p1 and p2 are pointing to same pointer Reset p1 p1 Reference Count = 0 p1 Reference Count = 1 p1 Reference Count = 0 p1 is NULL
Summary
The shared_ptr
Smart Pointer is ideal when you need shared ownership semantics in your C++ programs. It is a perfect example of modern C++’s commitment to safer resource management through RAII (Resource Acquisition Is Initialization).
Great Tutorials…Concise and Precise…and easily understandable.
Nice tutorials.. exact matter and understandable conceptual examples.
Thanks for the tutorial, much helpful.
Great Learning