What is shared_ptr in C++?

In this article, we will discuss one of the most used Smart Pointers in C++: shared_ptr.

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

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 using weak_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:

  1. For the integer: The memory needed to store the actual integer.
  2. 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 the delete 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).

4 thoughts on “What is shared_ptr in C++?”

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top