std::shared_ptr
std::shared_ptr in C++: An Overview
std::shared_ptr in C++: An Overviewstd::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
std::shared_ptrTo enable shared ownership of resources, allowing multiple
std::shared_ptrinstances 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
deleteoperations.
Key Features of std::shared_ptr
std::shared_ptr1. Shared Ownership
Multiple
std::shared_ptrinstances can share ownership of the same resource.The object is destroyed only when the last
std::shared_ptrmanaging 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_ptruses a control block to keep track of how manystd::shared_ptrinstances share ownership of the object.When a new
std::shared_ptris created (copy or assignment), the reference count is incremented.When a
std::shared_ptris 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
std::shared_ptrUnderstanding thread safety in std::shared_ptr involves distinguishing between two concepts:
Thread-Safe Reference Counting: Operations that modify the reference count (like copying, assigning, or destroying
std::shared_ptrinstances) are thread-safe.Thread Safety of the Managed Object: Accessing or modifying the object managed by
std::shared_ptris 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_ptrwithout 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:
Each thread receives a copy of
sp.Internally, the reference count is incremented for each copy and decremented when a thread’s local
std::shared_ptris destroyed.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:
Without the
std::mutex, both threads might read and write to the managedintconcurrently, causing a data race.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:
The destructor of the managed object is called automatically.
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
Destruction is Thread-Safe:
When the last
std::shared_ptris 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.
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
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_ptrsupports 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::weak_ptrstd::shared_ptrcan work alongsidestd::weak_ptr, a non-owning smart pointer that can access astd::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_ptrcan manage dynamically allocated arrays, though it is less common thanstd::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_ptrmanages 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_ptrdue to the reference counting mechanism.
8. Interoperability with Raw Pointers
You can construct a
std::shared_ptrfrom a raw pointer, but this is discouraged in favor ofstd::make_shared.Avoid passing the same raw pointer to multiple
std::shared_ptrinstances to prevent double-deletion errors.
std::shared_ptr<int> ptr(new int(42)); // Not recommended9. std::make_shared
std::make_sharedThis function is the preferred way to create
std::shared_ptrinstances 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); // RecommendedMethods 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 anotherstd::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_ptrin conjunction withstd::shared_ptrto break cyclic dependencies in graphs, trees, or other complex data structures.
Limitations
Overhead: Reference counting introduces some runtime and memory overhead.
Not for Circular Dependencies: If two
std::shared_ptrinstances reference each other, it creates a memory leak.Requires Careful Ownership Management: Using raw pointers with
std::shared_ptrcan 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