std::shared_ptr
Smart pointer with shared ownership via reference counting. Multiple shared_ptrs can own the same object, deleted when last owner destroyed.
Each copy increments count, each destruction decrements it. Count reaches zero β automatic deletion
auto ptr1 = std::make_shared<int>(42); // count = 1
auto ptr2 = ptr1; // count = 2
ptr1.reset(); // count = 1
ptr2.reset(); // count = 0 β delete
Reference Counting Modelβ
Control Block contains:
ββββββββββββββββββββββββ
β Reference Count: 3 β
β Weak Count: 0 β
β Deleter β
β Allocator β
ββββββββββββββββββββββββ
Basic Usageβ
Creating and copying shared_ptrs is straightforward. All copies share ownership of the same object.
#include <memory>
// Create shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
// Copy shares ownership
auto ptr2 = ptr1;
std::cout << ptr1.use_count(); // 2
std::cout << ptr2.use_count(); // 2
// Both point to same object
*ptr2 = 100;
std::cout << *ptr1; // 100
// Last owner deletes
ptr1.reset(); // count = 1
ptr2.reset(); // count = 0 β delete
Copying a shared_ptr increments the reference count. The object persists as long as any shared_ptr owns it. When the last shared_ptr is destroyed, the reference count reaches zero and the object is automatically deleted.
make_shared (C++11)β
Always prefer std::make_shared over direct new for efficiency and exception safety.
// β
Preferred: make_shared (single allocation)
auto ptr1 = std::make_shared<int>(42);
auto ptr2 = std::make_shared<std::string>("hello");
// β Avoid: direct new (two allocations)
std::shared_ptr<int> ptr3(new int(42));
make_shared allocates the object and control block (containing reference count) in a single memory allocation, which is more efficient than separate allocations. It's also exception-safe, preventing leaks if an exception occurs during construction.
Reference Countingβ
shared_ptr maintains a reference count tracking how many shared_ptrs own the object.
auto ptr1 = std::make_shared<int>(42);
// Check owner count
std::cout << ptr1.use_count(); // 1
{
auto ptr2 = ptr1; // Copy
auto ptr3 = ptr1; // Copy
std::cout << ptr1.use_count(); // 3
ptr2.reset(); // Decrement
std::cout << ptr1.use_count(); // 2
} // ptr3 destroyed
std::cout << ptr1.use_count(); // 1
// Object still alive
Each shared_ptr can query the current reference count with use_count(). When the count drops to zero, the managed object is deleted. The count is shared across all copies through the control block.
Thread Safetyβ
Reference count updates are thread-safe (atomic), but the pointed-to object is not automatically protected.
std::shared_ptr<int> global_ptr = std::make_shared<int>(42);
void thread1() {
auto local = global_ptr; // β
Thread-safe copy
// Reference count increment is atomic
}
void thread2() {
auto local = global_ptr; // β
Thread-safe copy
*local = 100; // β Data race if thread1 also modifies!
}
Copying shared_ptrs between threads is safe - the reference count operations are atomic. However, if multiple threads access the pointed-to object, you need additional synchronization (mutex, atomic operations) to protect the data.
- β Control block operations (ref counting) are atomic
- β Object itself is NOT automatically protected
- Need mutex/atomics to protect shared data
Shared Ownership Patternsβ
Multiple shared_ptrs can own the same object, useful for shared resources and graph structures.
Multiple Ownersβ
class Node {
public:
std::string data;
std::vector<std::shared_ptr<Node>> neighbors;
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
auto node3 = std::make_shared<Node>();
// Graph structure
node1->neighbors.push_back(node2);
node1->neighbors.push_back(node3);
node2->neighbors.push_back(node1); // Multiple owners
std::cout << node1.use_count(); // 2 (node1 + node2->neighbors)
This enables building complex data structures where multiple objects reference the same sub-object. The shared object persists as long as any owner exists.
Cache Patternβ
class ResourceCache {
std::map<std::string, std::shared_ptr<Resource>> cache;
public:
std::shared_ptr<Resource> get(const std::string& key) {
auto it = cache.find(key);
if (it != cache.end()) {
return it->second; // Return shared ownership
}
auto resource = std::make_shared<Resource>(key);
cache[key] = resource;
return resource;
}
};
Key Operationsβ
auto ptr = std::make_shared<int>(42);
// Access
*ptr = 100;
int* raw = ptr.get();
// Check validity
if (ptr) {
std::cout << *ptr;
}
// Owner count
long count = ptr.use_count();
// Reset (release ownership)
ptr.reset(); // Decrement count
ptr.reset(new int(200)); // Take new ownership
// Unique ownership check
if (ptr.use_count() == 1) {
// Only owner - can modify safely
}
Function Parametersβ
Different parameter types express different ownership and lifetime requirements.
// Observe: doesn't extend lifetime
void observe(const Widget* w) {
w->inspect();
}
// Use: doesn't affect ownership (preferred)
void use(const Widget& w) {
w->process();
}
// Share: participates in ownership
void share(std::shared_ptr<Widget> w) {
// Keeps object alive during function
}
// Observe through shared_ptr (unusual)
void observe_shared(const std::shared_ptr<Widget>& w) {
w->inspect();
}
auto widget = std::make_shared<Widget>();
observe(widget.get()); // Just observing
use(*widget); // Using temporarily
share(widget); // Sharing ownership (increment count)
observe_shared(widget); // Observing (no count change)
Pass by raw pointer or reference when the function doesn't need ownership. Pass by shared_ptr value when the function should extend the object's lifetime (store it, pass to async operations). Pass by const reference to shared_ptr when you need to check/copy the shared_ptr itself without affecting ownership.
| Intent | Type | Overhead |
|---|---|---|
| Observe | const T* or const T& | None |
| Share | shared_ptr<T> (by value) | Atomic inc/dec |
| Check/copy ptr | const shared_ptr<T>& | None |
Circular References Problemβ
shared_ptr can create circular references that prevent deletion, causing memory leaks.
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // β Creates cycle!
~Node() { std::cout << "~Node\n"; }
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node1 β node2
node2->prev = node1; // node2 β node1 (cycle!)
// When scope ends:
// - node1's count: 1 (node2->prev keeps it alive)
// - node2's count: 1 (node1->next keeps it alive)
// - Neither ever deleted β MEMORY LEAK!
Each node keeps the other alive through its shared_ptr. When the original shared_ptrs go out of scope, the reference counts only drop to 1 (the circular reference), never reaching zero. The objects are never deleted - a memory leak despite using smart pointers.
node1 (count=2) ββnextββ> node2 (count=2)
^ |
| |
βββββββββprevββββββββ
Both keep each other alive β never deleted
Breaking Cycles with weak_ptrβ
Use weak_ptr to break circular references (covered in the next section).
class Node {
public:
std::shared_ptr<Node> next; // Strong reference forward
std::weak_ptr<Node> prev; // β
Weak reference back
~Node() { std::cout << "~Node\n"; }
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // Strong: count = 2
node2->prev = node1; // Weak: count stays 1
// Destructors called correctly!
Polymorphismβ
shared_ptr works naturally with inheritance and polymorphism.
class Base {
public:
virtual ~Base() { std::cout << "~Base\n"; }
virtual void identify() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
~Derived() override { std::cout << "~Derived\n"; }
void identify() override { std::cout << "Derived\n"; }
};
std::shared_ptr<Base> ptr1 = std::make_shared<Derived>();
ptr1->identify(); // "Derived"
std::shared_ptr<Base> ptr2 = ptr1; // Share ownership
std::cout << ptr1.use_count(); // 2
// Proper cleanup: ~Derived, then ~Base
The virtual destructor ensures correct cleanup. All shared_ptrs can share ownership of the derived object through base class pointers, and cleanup happens correctly when the last owner is destroyed.
Containers of shared_ptrβ
Containers can hold shared_ptrs, enabling collections where multiple containers can reference the same objects.
class Widget {
public:
int id;
Widget(int i) : id(i) {}
};
std::vector<std::shared_ptr<Widget>> all_widgets;
std::vector<std::shared_ptr<Widget>> active_widgets;
auto w1 = std::make_shared<Widget>(1);
auto w2 = std::make_shared<Widget>(2);
all_widgets.push_back(w1);
all_widgets.push_back(w2);
active_widgets.push_back(w1); // w1 shared between both vectors
std::cout << w1.use_count(); // 3 (w1 variable + 2 vectors)
std::cout << w2.use_count(); // 2 (w2 variable + all_widgets)
Objects persist as long as any container holds a shared_ptr to them. This enables flexible ownership patterns where objects can be referenced from multiple collections.
aliasing Constructorβ
shared_ptr supports aliasing - storing one pointer but referencing another, useful for managing members of an object.
struct Widget {
int value;
Widget(int v) : value(v) {}
};
auto widget = std::make_shared<Widget>(42);
// Aliased shared_ptr to member
std::shared_ptr<int> value_ptr(widget, &widget->value);
std::cout << *value_ptr; // 42
std::cout << widget.use_count(); // 2 (widget and value_ptr)
value_ptr.reset();
std::cout << widget.use_count(); // 1
// widget keeps the object alive as long as value_ptr exists
The aliased shared_ptr shares ownership of the Widget but points to its member. This ensures the Widget stays alive as long as any pointer to its members exists.
Custom Deletersβ
shared_ptr supports custom deleters for non-standard cleanup, passed at construction time.
auto deleter = [](FILE* f) {
if (f) {
std::cout << "Closing file\n";
fclose(f);
}
};
std::shared_ptr<FILE> file(fopen("data.txt", "r"), deleter);
// File closed when last shared_ptr destroyed
Unlike unique_ptr, the deleter type isn't part of shared_ptr's type, making it easier to work with. We'll cover custom deleters in detail in the next section.
Performance Considerationsβ
shared_ptr has overhead compared to unique_ptr and raw pointers due to reference counting.
// Size overhead
sizeof(std::shared_ptr<int>); // 16 bytes (2 pointers)
sizeof(std::unique_ptr<int>); // 8 bytes (1 pointer)
sizeof(int*); // 8 bytes
// Runtime overhead
// - Atomic reference counting
// - Control block allocation (unless make_shared)
// - Indirection through control block
The control block contains the reference count and weak count, requiring extra memory. Atomic increment/decrement operations for thread safety are slower than simple pointer copies. Use shared_ptr when you need shared ownership; prefer unique_ptr for exclusive ownership (cheaper).
Cost breakdown:
- Copy: atomic increment (~50 cycles)
- Destruction: atomic decrement (~50 cycles)
- Dereference: same as raw pointer
- Creation: allocation + atomic init
Use unique_ptr unless you need shared ownership:
- Faster (no atomic operations)
- Smaller (8 vs 16 bytes)
- Clearer ownership semantics
enable_shared_from_thisβ
class Widget : public std::enable_shared_from_this<Widget> {
public:
std::shared_ptr<Widget> getShared() {
return shared_from_this(); // β
Safe
}
void registerCallback() {
auto self = shared_from_this();
callbacks.push_back([self]() {
self->doWork(); // Keeps Widget alive
});
}
};
// β
Correct usage
auto w = std::make_shared<Widget>();
auto shared = w->getShared();
// β Wrong - creates second control block
std::shared_ptr<Widget> bad(this); // DISASTER!
class Bad {
std::shared_ptr<Bad> getPtr() {
return std::shared_ptr<Bad>(this); // β Second control block!
}
};
auto ptr1 = std::make_shared<Bad>();
auto ptr2 = ptr1->getPtr(); // Two control blocks β double delete!
Summaryβ
Core features:
- Shared ownership (multiple owners)
- Reference counting (automatic)
- Thread-safe counting (atomic operations)
- Last owner deletes object
Key operations:
make_shared<T>(args)- create (preferred)ptr.use_count()- check owner countptr.reset()- release ownership- Copy shares ownership (count++)
Circular references:
shared_ptrcycles cause leaks- Use
weak_ptrto break cycles - Parentβchild: strong
- Childβparent: weak
Performance:
- 16 bytes (vs 8 for
unique_ptr) - Atomic operations overhead
- Use only when truly need shared ownership
Best practices:
- Prefer
unique_ptrby default - Use
make_sharedfor efficiency - Break cycles with
weak_ptr enable_shared_from_thisforthissharing- Pass by const& to observe without cost