- 1. Overview
- 2. std::unique_ptr
- 3. std::shared_ptr
- 4. std::weak_ptr
- 5. Checking for Null
- 6. Pointer Casting
- 7. Atomic Smart Pointers (C++20)
- 8. See Also
A smart pointer is a class that owns a heap object and frees it automatically. The C++ standard library provides three:
| Type | Ownership | Cost | Use when |
|---|---|---|---|
std::unique_ptr<T> |
Sole, transferable | Same as a raw pointer | Default choice for owning a heap resource. |
std::shared_ptr<T> |
Shared via reference count | Two pointers + atomic refcount | Multiple parts of the program genuinely co-own the object. |
std::weak_ptr<T> |
None — observes a shared_ptr |
Same as shared_ptr |
Break cycles; check whether a shared object is still alive. |
Default to unique_ptr. Reach for shared_ptr only when ownership truly is shared. See shared_ptr_use_cases.md for concrete patterns where shared ownership is justified.
A unique_ptr<T> owns exactly one T allocation. When the unique_ptr is destroyed (or reset, or moved-from), the T is destroyed too. It has the same memory footprint as a raw pointer.
#include <memory>
// ✅ Preferred — exception-safe, single allocation possible for shared_ptr
auto p = std::make_unique<Person>("Alice", 30);
// Also valid, but two-step and not exception-safe in expressions
std::unique_ptr<Person> q(new Person("Bob", 25));
// Empty
std::unique_ptr<Person> r; // r == nullptrstd::make_unique (C++14) is the default; it forwards arguments to T's constructor.
A unique_ptr cannot be copied — copying would create two owners, defeating the point.
auto a = std::make_unique<Person>();
auto b = a; // ❌ compile error: deleted copy ctor
auto c = std::move(a); // ✅ ownership moves; a is now emptyThis makes unique_ptr cheap to pass around: pass by value and std::move to transfer ownership.
| Method | What it does |
|---|---|
p.reset() |
Deletes the owned object, sets p to nullptr. |
p.reset(q) |
Deletes the old object, then takes ownership of raw pointer q. |
p.release() |
Does not delete. Returns the raw pointer and sets p to nullptr. Caller now owns it. |
p.get() |
Returns the raw pointer without giving up ownership. Don't delete it. |
auto u = std::make_unique<int>(42);
int* raw = u.release(); // u is empty; YOU now own *raw
delete raw; // must delete manually
u.reset(new int(7)); // u now owns a fresh int
u.reset(); // deletes the int, u is emptyrelease() is rare in modern code — usually means you're handing ownership to a C API.
unique_ptr accepts a deleter as a second template argument. This makes it perfect for managing C resources:
auto file = std::unique_ptr<FILE, decltype(&fclose)>(fopen("data.txt", "r"), &fclose);
// ^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^
// managed type deleter type deleter
// fclose is called automatically when `file` goes out of scopeOr with a lambda:
auto window = std::unique_ptr<SDL_Window, void(*)(SDL_Window*)>(
SDL_CreateWindow(...), SDL_DestroyWindow);Note: the deleter type is part of the unique_ptr's type. A unique_ptr<FILE, decltype(&fclose)> is not the same type as unique_ptr<FILE>.
A shared_ptr<T> participates in a reference-counted lifetime. Every copy bumps the count; the last to be destroyed deletes the object.
auto a = std::make_shared<Person>("Alice");
auto b = a; // refcount = 2
{
auto c = a; // refcount = 3
} // refcount = 2 again
// when a and b both go out of scope, Person is deletedA shared_ptr is two pointers wide: one to the object, one to a heap-allocated control block:
shared_ptr A ─┐ ┌─ shared_ptr B
│ │
├─► Object ◄──────────────────────┤
│ │
└─► Control Block ◄───────────────┘
├── strong refcount
├── weak refcount
├── deleter
└── allocator
Every copy of the shared_ptr shares the same control block. The block lives until the last weak_ptr to it is gone (so weak pointers can check if the object is dead).
// ✅ Preferred — single allocation for both object and control block
auto p = std::make_shared<Person>("Alice");
// Two allocations: one for Person, another for the control block
auto q = std::shared_ptr<Person>(new Person("Bob"));make_shared is faster (one allocation, better cache locality) and exception-safe. The downside: the object's memory isn't freed until the last weak_ptr is gone, because the control block lives in the same allocation. If you have weak pointers far outliving the object and the object is large, prefer the two-allocation form.
auto p = std::make_shared<int>(42);
std::cout << p.use_count(); // 1 — number of shared_ptrs to this object
p.reset(); // deletes if this was the last owner
p.reset(new int(7)); // owns a new int nowuse_count() is approximate in multithreaded code — by the time you read it, another thread may have changed it. Don't gate logic on it.
A common bug:
class Worker {
public:
void schedule() {
scheduler.push(std::shared_ptr<Worker>(this)); // ⚠️ second control block!
}
};
auto w = std::make_shared<Worker>();
w->schedule();
// Now there are TWO control blocks managing the same Worker.
// When either hits zero refs, it deletes the Worker — the other one then
// has a dangling pointer. Crash.The fix: inherit from std::enable_shared_from_this<T> and call shared_from_this():
class Worker : public std::enable_shared_from_this<Worker> {
public:
void schedule() {
scheduler.push(shared_from_this()); // ✅ shares the existing control block
}
};Caveat: shared_from_this() only works after the object is already owned by some shared_ptr. Calling it from the constructor throws std::bad_weak_ptr.
If two objects each hold a shared_ptr to the other, neither refcount ever reaches zero — the cycle leaks forever:
struct Node {
std::shared_ptr<Node> next; // ⚠️ if next->next points back to this, leak
};Solution: make at least one direction a weak_ptr. See §4.2.
A weak_ptr<T> is a non-owning observer of a shared_ptr. It doesn't keep the object alive; it just lets you ask "is the object still there?"
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // observes sp, doesn't own
if (auto locked = wp.lock()) { // returns shared_ptr — empty if expired
std::cout << *locked; // safe to use here
} else {
std::cout << "object is gone";
}
sp.reset(); // destroys the int
std::cout << wp.expired(); // trueTwo ways to check:
wp.expired()— bool, racy in multithreaded code (the answer can change before you act).wp.lock()— atomic; returns ashared_ptrif alive, empty if not. Always prefer this when you intend to use the object.
A parent–child tree where children need to point back at their parent:
struct Node {
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr<Node> parent; // ✅ weak — breaks the cycle
};
auto root = std::make_shared<Node>();
auto child = std::make_shared<Node>();
root->children.push_back(child);
child->parent = root; // weak assignment from sharedGoing up the tree: if (auto p = child->parent.lock()) { ... }.
The general rule: when ownership has a clear direction (parent owns children, observer observes subject), the back-reference should be weak_ptr.
All three smart pointers are contextually convertible to bool:
if (p) { /* not null */ }
if (!p) { /* null */ }
if (p != nullptr) { /* same thing */ }For weak_ptr specifically, check via lock() (see §4.1) — weak_ptr itself is not bool-convertible.
For polymorphic types you can cast through smart pointers without losing the refcount:
struct Base { virtual ~Base() = default; };
struct Derived : Base {};
std::shared_ptr<Base> b = std::make_shared<Derived>();
auto d1 = std::static_pointer_cast<Derived>(b); // unchecked
auto d2 = std::dynamic_pointer_cast<Derived>(b); // checked, returns empty if wrong type
auto cb = std::const_pointer_cast<Base>(b); // strip constAll three return a new shared_ptr that shares the same control block — refcount stays correct.
unique_ptr has no cast helpers; if you need to downcast you must move and re-wrap manually (rarely needed).
Pre-C++20, you needed std::atomic_load(&sp) / std::atomic_store(&sp, …) (now deprecated) to safely share a shared_ptr variable across threads — note: this is about the pointer object itself, not the pointee. Multiple threads reading and writing the same sp variable was a data race.
C++20 adds proper specializations:
std::atomic<std::shared_ptr<Config>> current_config;
// thread A — publish
current_config.store(std::make_shared<Config>(load()));
// thread B — read latest
auto cfg = current_config.load(); // gets a fresh shared_ptr safely
cfg->use();This is the right primitive for "hot reload"-style configuration where many readers occasionally see a new pointer.
A separate std::atomic<std::weak_ptr<T>> exists for the same reason on weak pointers.
- shared_ptr_use_cases.md — twelve patterns where shared ownership is the right tool, each with minimal code.
- passing_returning_smart_pointers_to_from_functions.md — function-signature rules (when to take
T*,T&,unique_ptr<T>,const shared_ptr<T>&). - smart_pointers_class_member.md — owning resources from class members.
- pointers.md — raw pointers, dangling, AddressSanitizer.
- references.md —
reference_wrapperfor storing references in containers.
References: