std::shared_ptr

std::shared_ptr in C++: An Overview

std::shared_ptr is a smart pointer introduced in C++11 as part of the Standard Template Library (STL) and resides in the <memory> header. It provides shared ownership of a dynamically allocated object, allowing multiple std::shared_ptr instances to manage the same resource. It uses reference counting to ensure the object is deleted when the last std::shared_ptr managing it is destroyed or reset.

Below is a detailed explanation of std::shared_ptr, its features, and use cases.


Purpose of std::shared_ptr

  • To enable shared ownership of resources, allowing multiple std::shared_ptr instances to share responsibility for a single dynamically allocated object.

  • To automate memory management through reference counting, ensuring the object is properly cleaned up when it is no longer in use.

  • To prevent memory leaks and eliminate the need for manual delete operations.


Key Features of std::shared_ptr

1. Shared Ownership

  • Multiple std::shared_ptr instances can share ownership of the same resource.

  • The object is destroyed only when the last std::shared_ptr managing it is destroyed or reset.

#include <iostream>
#include <memory>

int main() {
    auto ptr1 = std::make_shared<int>(10);
    auto ptr2 = ptr1; // Shared ownership
    std::cout << *ptr1 << ", " << *ptr2 << std::endl;

    return 0; // The managed object is deleted automatically.
}

2. Reference Counting

  • std::shared_ptr uses a control block to keep track of how many std::shared_ptr instances share ownership of the object.

  • When a new std::shared_ptr is created (copy or assignment), the reference count is incremented.

  • When a std::shared_ptr is destroyed or reset, the reference count is decremented.

  • When the reference count reaches zero, the object is deleted.

You can observe the reference count using the use_count() method:

#include <iostream>
#include <memory>

int main() {
    auto ptr1 = std::make_shared<int>(10);
    auto ptr2 = ptr1; // Shared ownership
    std::cout << "Reference count: " << ptr1.use_count() << std::endl; // Output: 2

    ptr2.reset(); // Decrease reference count
    std::cout << "Reference count after reset: " << ptr1.use_count() << std::endl; // Output: 1

    return 0;
}

3. Thread-Safe Reference Counting

  • Incrementing and decrementing the reference count are thread-safe.

  • However, access to the managed object itself is not thread-safe and requires external synchronization if used in a multithreaded environment.

Thread Safety in std::shared_ptr

Understanding thread safety in std::shared_ptr involves distinguishing between two concepts:

  1. Thread-Safe Reference Counting: Operations that modify the reference count (like copying, assigning, or destroying std::shared_ptr instances) are thread-safe.

  2. Thread Safety of the Managed Object: Accessing or modifying the object managed by std::shared_ptr is not thread-safe and requires external synchronization.


1. Thread-Safe Reference Counting

  • When you create, assign, or destroy a std::shared_ptr, the reference count is incremented or decremented automatically.

  • These operations are thread-safe, meaning multiple threads can safely share a std::shared_ptr without corrupting the reference count.

  • If the reference count drops to zero, the managed object is destroyed automatically in a thread-safe manner.

For example:

#include <iostream>
#include <memory>
#include <thread>
#include <vector>

void threadFunction(std::shared_ptr<int> sp) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Thread: " << *sp << "\n";
}

int main() {
    auto sp = std::make_shared<int>(42);

    // Multiple threads sharing the same std::shared_ptr
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(threadFunction, sp); // Thread-safe reference counting
    }

    for (auto& t : threads) t.join();

    // When all threads are done, the reference count drops to 0, and the object is destroyed.
    return 0;
}

Explanation:

  1. Each thread receives a copy of sp.

  2. Internally, the reference count is incremented for each copy and decremented when a thread’s local std::shared_ptr is destroyed.

  3. When the last thread exits and the reference count reaches 0, the managed object (int) is destroyed safely.


2. Managed Object is NOT Thread-Safe

While reference counting is thread-safe, the object pointed to by the std::shared_ptr is not thread-safe by default. This means that if multiple threads access or modify the managed object, you must ensure synchronization (e.g., using std::mutex).

For example:

#include <iostream>
#include <memory>
#include <thread>
#include <mutex>

std::mutex mtx;

void incrementValue(std::shared_ptr<int> sp) {
    for (int i = 0; i < 5; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // Synchronize access
        (*sp)++;
        std::cout << "Value: " << *sp << "\n";
    }
}

int main() {
    auto sp = std::make_shared<int>(0);

    std::thread t1(incrementValue, sp);
    std::thread t2(incrementValue, sp);

    t1.join();
    t2.join();

    return 0;
}

Explanation:

  1. Without the std::mutex, both threads might read and write to the managed int concurrently, causing a data race.

  2. Using std::lock_guard<std::mutex>, we ensure that only one thread can access the managed object at a time.


Why is the Managed Object Not Thread-Safe?

std::shared_ptr does not enforce thread safety for the object because:

  • It doesn't know how the object will be used.

  • Adding internal synchronization for every access would incur unnecessary performance overhead for cases where synchronization is not needed.


What Happens When the Reference Count Reaches Zero?

When the reference count reaches zero:

  1. The destructor of the managed object is called automatically.

  2. This destruction is thread-safe and occurs exactly once, even if multiple threads are decrementing the reference count concurrently.

For example:

#include <iostream>
#include <memory>
#include <thread>

struct Resource {
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

void threadFunction(std::shared_ptr<Resource> sp) {
    // Thread work
}

int main() {
    auto sp = std::make_shared<Resource>();

    std::thread t1(threadFunction, sp);
    std::thread t2(threadFunction, sp);

    t1.join();
    t2.join();

    // When both threads are done, the reference count drops to 0,
    // and the Resource destructor is called safely.
    return 0;
}

Important Notes

  1. Destruction is Thread-Safe:

    • When the last std::shared_ptr is destroyed, the managed object is destroyed in a thread-safe manner.

    • This ensures there is no double-destruction or access to an already-destroyed object.

  2. Access is NOT Thread-Safe:

    • If threads need to modify or read the managed object simultaneously, external synchronization (e.g., std::mutex) is required.


Summary Table

Operation
Thread-Safe?
Notes

Increment/Decrement Reference Count

Yes

Copying, assigning, or destroying std::shared_ptr is thread-safe.

Accessing the Managed Object

No

You need to use external synchronization like std::mutex.

Destroying the Managed Object

Yes

Happens automatically when reference count drops to zero.

std::shared_ptr is designed to provide thread-safe reference management, but you are responsible for ensuring that the object's content is accessed safely in a multithreaded environment.


4. Custom Deleters

  • std::shared_ptr supports custom deleters, allowing you to define how the managed object is destroyed.

  • Custom deleters are useful for managing non-heap resources, such as file handles or sockets.

#include <iostream>
#include <memory>

void customDeleter(int* p) {
    std::cout << "Custom deleter called for " << *p << std::endl;
    delete p;
}

int main() {
    std::shared_ptr<int> ptr(new int(42), customDeleter);

    return 0; // Custom deleter is called automatically.
}

5. Compatible with std::weak_ptr

  • std::shared_ptr can work alongside std::weak_ptr, a non-owning smart pointer that can access a std::shared_ptr-managed object without affecting its reference count.

  • This helps to avoid cyclic dependencies, which can lead to memory leaks.

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> shared = std::make_shared<int>(42);
    std::weak_ptr<int> weak = shared;

    if (auto locked = weak.lock()) {
        std::cout << "Weak pointer locked: " << *locked << std::endl;
    }

    return 0;
}

6. Supports Arrays

  • std::shared_ptr can manage dynamically allocated arrays, though it is less common than std::unique_ptr<T[]>.

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int[]> arr(new int[5], std::default_delete<int[]>());
    for (int i = 0; i < 5; ++i) arr[i] = i;

    for (int i = 0; i < 5; ++i) std::cout << arr[i] << " ";
    std::cout << std::endl;

    return 0;
}

7. Lightweight and Efficient

  • std::shared_ptr manages both the pointer to the resource and the control block containing the reference count.

  • It is relatively efficient but has slightly more overhead than std::unique_ptr due to the reference counting mechanism.


8. Interoperability with Raw Pointers

  • You can construct a std::shared_ptr from a raw pointer, but this is discouraged in favor of std::make_shared.

  • Avoid passing the same raw pointer to multiple std::shared_ptr instances to prevent double-deletion errors.

std::shared_ptr<int> ptr(new int(42)); // Not recommended

9. std::make_shared

  • This function is the preferred way to create std::shared_ptr instances because it combines resource allocation and control block creation into a single operation.

  • It is safer, more efficient, and eliminates the risk of dangling pointers.

auto ptr = std::make_shared<int>(42); // Recommended

Methods and Operations

Constructors

  • Default: std::shared_ptr<T>()

  • Null pointer: std::shared_ptr<T>(nullptr)

  • Take ownership: std::shared_ptr<T>(T* ptr)

  • Custom deleter: std::shared_ptr<T>(T* ptr, Deleter d)

Accessors

  • get(): Returns the raw pointer without affecting ownership.

  • use_count(): Returns the current reference count.

  • operator*: Dereferences the managed pointer.

  • operator->: Accesses members of the managed object.

Modifiers

  • reset(): Releases ownership and optionally assigns a new object.

  • swap(): Exchanges ownership with another std::shared_ptr.


Common Use Cases

1. Shared Ownership

  • Suitable for scenarios where multiple parts of a program need shared access to the same resource.

2. Polymorphic Object Management

  • Works seamlessly with polymorphic types, ensuring proper destruction of derived objects.

struct Base { virtual ~Base() = default; };
struct Derived : Base {};
std::shared_ptr<Base> ptr = std::make_shared<Derived>();

3. Circular Dependency Prevention with std::weak_ptr

  • Use std::weak_ptr in conjunction with std::shared_ptr to break cyclic dependencies in graphs, trees, or other complex data structures.


Limitations

  1. Overhead: Reference counting introduces some runtime and memory overhead.

  2. Not for Circular Dependencies: If two std::shared_ptr instances reference each other, it creates a memory leak.

  3. Requires Careful Ownership Management: Using raw pointers with std::shared_ptr can lead to double-deletion or dangling pointers.


Comparison with Other Smart Pointers

Feature

std::unique_ptr

std::shared_ptr

std::weak_ptr

Ownership Model

Exclusive

Shared

Non-owning

Reference Counting

No

Yes

Yes (with std::shared_ptr)

Copyable

No

Yes

No

Overhead

Minimal

Moderate (due to ref count)

Minimal

Use Case

Single owner

Shared ownership

Non-owning reference


Code Example: Comprehensive Usage

#include <iostream>
#include <memory>

struct Resource {
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main() {
    auto ptr1 = std::make_shared<Resource>(); // Create shared pointer
    auto ptr2 = ptr1;                         // Share ownership

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

    ptr2.reset(); // Decrease reference count
    std::cout << "Reference count after reset: " << ptr1.use_count() << std::endl; // Output: 1

    return 0;
    // Resource is destroyed automatically when ptr1 goes out of scope.
}

Conclusion

std::shared_ptr is an essential tool in modern C++ for managing shared ownership of dynamically allocated objects. It provides a robust way to handle memory management in shared scenarios, reducing the risk of memory leaks and making your code safer and more maintainable.

Last updated