Updated on 18 Jun, 202527 mins read 42 views

Understanding std::shared_ptr

std::shared_ptr is a smart pointer that provides shared ownership of dynamically allocated objects. Unlike std::unique_ptr, which enforces exclusive ownership, std::shared_ptr instances to share ownership of the same resource. It employs a reference counting mechanism to keep track of the number of std::shared_ptr instances pointing to a particular object automatically deallocates the memory when the last shared pointer goes out of scope.

Syntax and Features

Declaration and Initialization:

#include <memory>

std::shared_ptr<int> ptr1 = std::make_shared<int>(42); // Preferred way

std::shared_ptr<int> ptr2(new int(42));                // Allowed, but avoid using raw new

The std::make_shared function is preferred for creating a std::shared_ptr as it ensures both memory allocation and the creation of the managed object occur together, enhancing performance.

Use std::make_shared whenever possible. It’s more efficient and exception-safe, as it creates the object and the control block (reference count) in one memory allocation.

Features

1 Shared Ownership:

std::shared_ptr allows multiple pointers to share ownership of the same dynamically allocated object. Each std::shared_ptr instance maintains a reference count, and the object is automatically deallocated when the last shared pointer pointing to it goes out of scope.

std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1; // Both share ownership

std::cout << *ptr1 << ", " << *ptr2 << std::endl; // Both access the same object

Both ptr1 and ptr2 point to the same memory, and the resource will only be released when both are destroyed or reset.

Example:

#include <memory>
#include <iostream>

int main() {
    // Creating a shared_ptr to manage dynamically allocated memory
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    
    std::shared_ptr<int> ptr2 = ptr1; // Shared ownership

    // Accessing the managed object through ptr2
    std::cout << *ptr2 << std::endl; // Output: 42

    // No need to explicitly delete memory, handled by shared_ptr

    return 0;
}

2 Reference Counting

std::shared_ptr uses reference counting to keep track of the number of shared pointers pointing to the same object. When a shared pointer is copied or destroyed, the reference count is updated accordingly.

You can check how many shared_ptr instances are sharing the resource using .use_count():

std::cout << "Use count: " << ptr1.use_count() << std::endl;

Example:

#include <memory>
#include <iostream>

int main() {
    // Creating a shared_ptr to manage dynamically allocated memory
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    
    std::shared_ptr<int> ptr2 = ptr1; // Shared ownership

    // Displaying the reference count
    std::cout << "Reference count: " << ptr1.use_count() << std::endl; // Output: 2

    return 0;
}

3 Automatic Memory Management

std::shared_ptr automatically deallocates the associated memory when the last shared pointer pointing to the object goes out of scope. This ensures proper cleanup and prevents memory leaks.

Example:

#include <memory>
#include <iostream>

void func() {
    std::shared_ptr<int> ptr = std::make_shared<int>(42);
} // ptr goes out of scope and memory is automatically deallocated

int main() {
    func(); // Memory deallocated after func() returns
    std::cout << "Memory deallocated successfully" << std::endl;

    return 0;
}

4 Control Block

std::shared_ptr uses a control block to store the reference count and manage shared ownership of the object. Each std::shared_ptr instance points to the control block, which in turn points to the dynamically allocated object.

Example:

#include <memory>
#include <iostream>

int main() {
    // Creating a shared_ptr to manage dynamically allocated memory
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    
    std::shared_ptr<int> ptr2 = ptr1; // Shared ownership

    // Displaying the address of the control block
    std::cout << "Control block address: " << ptr1.get() << std::endl;

    return 0;
}
// Output
Control block address: 0x131a2c0

5 Resetting and Releasing

You can manually release ownership using reset():

ptr1.reset();  // Decreases the reference count

Note: shared_ptrdoes not have a release() method like unique_ptr — this is intentional, to avoid unsafe use of raw pointers.

Custom Deleters with shared_ptr

Like unique_ptr, shared_ptr also supports custom deleters, allowing safe cleanup for non-memory resources:

auto fileCloser = [](FILE* f) {
    if (f) fclose(f);
};

std::shared_ptr<FILE> file(fopen("log.txt", "r"), fileCloser);

This ensures fclose is called when the last shared_ptr owning the file is destroyed.

Example Usage

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> a = std::make_shared<int>(100);
    std::shared_ptr<int> b = a;

    std::cout << "Value: " << *a << std::endl;
    std::cout << "Use count: " << a.use_count() << std::endl;

    b.reset();

    std::cout << "Use count after reset: " << a.use_count() << std::endl;

    return 0;
}

Output:

Value: 100
Use count: 2
Use count after reset: 1

Benefits of std::shared_ptr

FeatureDescription
🤝 Shared OwnershipMultiple smart pointers can manage the same resource.
🔁 Automatic CleanupDeletes the object only when all shared owners are gone.
🧠 Reference CountingTracks how many owners are sharing the object.
🔧 Custom DeletersCleanly manage files, sockets, or other resources.
💡 Thread-Safe Reference CountingSafe to use across threads (object access still needs synchronization).

When Not to Use shared_ptr

  • When you don’t need shared ownership — prefer unique_ptr to avoid overhead.
  • In cyclic dependencies (e.g., two shared_ptrs pointing to each other) — this causes memory leaks. Use std::weak_ptr to break the cycle.

Problem with shared_ptr

1 Cyclic (Circular) References - Memory Leaks

The most notorious issue with shared_ptr is that cyclic references never get freed. If two or more shared_ptrs reference each other, their reference counts never drop to zero, even when they go out of scope.

struct B;
struct A {
    std::shared_ptr<B> b;
};
struct B {
    std::shared_ptr<A> a;
};

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;  // ❌ Memory leak — circular reference

2. 🐢 Performance Overhead

shared_ptr uses a control block that tracks reference counts. This adds:

  • Memory overhead (the control block itself)

  • CPU overhead (atomic operations on the reference counter, which are thread-safe but slower)

This makes shared_ptrheavier than unique_ptr, especially in tight loops or performance-critical systems.

3 Incorrect Use of new Instead of make_shared

Manually calling new and passing the raw pointer to a shared_ptr is error-prone and less efficient:

std::shared_ptr<MyClass> p(new MyClass); // ❌ okay but suboptimal

Issues:

  • You may accidentally create two separate shared_ptrs managing the same raw pointer — leading to double deletion.

  • It performs two allocations: one for the object, one for the control block.

Solution:

Use std::make_shared:

auto p = std::make_shared<MyClass>(); // ✅ safer and more efficient

4. ❌ Dangling Shared Pointers

If a shared_ptr is constructed from a raw pointer that is also managed elsewhere, or from a local stack variable, it may lead to undefined behavior or double deletion.

int* x = new int(5);
std::shared_ptr<int> a(x);
std::shared_ptr<int> b(x); // ❌ Two shared_ptrs managing the same raw pointer

Solution:

Avoid directly using new unless absolutely necessary. Prefer make_shared, and never pass the same raw pointer to multiple shared_ptrs.