std::shared_ptr: A Practical Guide
C++ std::shared_ptr: A Practical Guide
Everything you need to use std::shared_ptr effectively: ownership semantics, control blocks, performance, and common pitfalls—with concise, runnable examples.
1) Basics: ownership and lifetime
#include <memory>
#include <cassert>
struct Foo { int x{0}; };
int main() {
std::shared_ptr<Foo> p = std::make_shared<Foo>();
assert(p.use_count() == 1);
{
std::shared_ptr<Foo> q = p; // shared ownership
assert(p.use_count() == 2);
q->x = 42;
} // q destroyed, count decremented
assert(p.use_count() == 1);
}
2) Control block: reference counts + deleter + allocator
- Created by
std::make_shared(preferred) or by constructingshared_ptrfrom a raw pointer - Holds strong count, weak count, and the deleter
- Reduced allocations with
make_shared(object + control block together)
auto p = std::make_shared<Foo>(); // fast, one allocation
auto q = std::shared_ptr<Foo>(new Foo()); // two allocations, avoid unless custom deleter needed
3) Custom deleters
#include <cstdio>
#include <memory>
struct FILECloser {
void operator()(FILE* f) const noexcept { if (f) std::fclose(f); }
};
int main() {
std::shared_ptr<FILE> file(std::fopen("data.txt", "r"), FILECloser{});
}
Or with a lambda:
auto closer = [](FILE* f){ if (f) std::fclose(f); };
std::shared_ptr<FILE> file(std::fopen("data.txt", "r"), closer);
4) weak_ptr: break cycles and observe without owning
#include <memory>
#include <cassert>
struct Node {
int v;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak to break cycle
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // no cycle because weak_ptr
auto pa = b->prev.lock(); // acquire shared ownership if still alive
assert(pa == a);
}
5) enable_shared_from_this: get a shared_ptr from this
#include <memory>
#include <cassert>
struct Self : std::enable_shared_from_this<Self> {
std::shared_ptr<Self> getPtr() { return shared_from_this(); }
};
int main() {
auto s = std::make_shared<Self>();
auto p = s->getPtr();
assert(p.use_count() == 2);
}
Avoid constructing an object managed by shared_ptr with a raw pointer to this before a shared_ptr exists—use make_shared.
6) Aliasing constructor: share ownership, different pointer
#include <memory>
#include <vector>
#include <cassert>
struct Obj { std::vector<int> data{1,2,3}; };
int main() {
auto owner = std::make_shared<Obj>();
// aliasing: shared ownership of owner, but points to underlying data
std::shared_ptr<int> element(owner, owner->data.data());
assert(owner.use_count() == 2);
}
7) Arrays: prefer std::vector or unique_ptr<T[]> over shared_ptr<T[]>
#include <memory>
// If you must use a shared array:
std::shared_ptr<int[]> arr(new int[10]{});
arr[0] = 1; // OK with shared_ptr<T[]>
But usually:
- Use
std::vector<T>for dynamic arrays - Or
std::unique_ptr<T[]>when unique ownership suffices
8) Thread-safety
- Multiple
shared_ptrinstances can be copied/destroyed concurrently (control block ops are atomic) - Access to the managed object itself is NOT thread-safe; guard with a mutex
- Prefer copying
shared_ptrinto threads by value if they need ownership
#include <thread>
#include <memory>
struct Foo { int x{0}; };
void worker(std::shared_ptr<Foo> p) {
// safe to copy/use p; protect p's internals if shared
}
int main() {
auto p = std::make_shared<Foo>();
std::thread t(worker, p); // copies p
t.join();
}
9) Performance tips
- Prefer
std::make_shared(fewer allocations, better cache locality) - Avoid
use_count()for logic; it can be expensive and racy across threads - Pass
shared_ptrby value only if you need to share ownership; else preferFoo&/Foo*/const Foo& - Avoid creating multiple independent control blocks for the same raw pointer (leads to double delete)
Foo* raw = new Foo();
std::shared_ptr<Foo> a(raw);
std::shared_ptr<Foo> b(raw); // BAD: two control blocks managing same raw
10) Common pitfalls
- Reference cycles (shared_ptr <-> shared_ptr) leak memory → use
weak_ptrfor back references - Using
shared_from_this()before object is owned by ashared_ptr→ undefined behavior - Capturing
thisraw in async tasks while object is owned elsewhere → prefer capturing ashared_ptr<Self> - Overusing
shared_ptrfor everything → choose the narrowest ownership (raw reference,unique_ptr, etc.)
11) Putting it all together
#include <memory>
#include <mutex>
#include <vector>
struct Image { /* ... */ };
struct Cache : std::enable_shared_from_this<Cache> {
std::mutex m;
std::vector<std::weak_ptr<Image>> images; // avoid retaining all images
std::shared_ptr<Image> loadImage() {
auto img = std::make_shared<Image>();
{
std::lock_guard<std::mutex> lock(m);
images.emplace_back(img);
}
return img;
}
std::shared_ptr<Cache> self() { return shared_from_this(); }
};
int main() {
auto cache = std::make_shared<Cache>();
auto img = cache->loadImage();
}
Use shared_ptr when multiple owners must co-manage an object’s lifetime. Prefer make_shared, use weak_ptr to prevent cycles, and guard the managed object with proper synchronization when shared across threads.