Smart Pointers Cheatsheet¶
Ownership should be visible in the type. A unique_ptr<T> says this owns a T; a T* says
this observes a T that somebody else owns. Choosing between them is a design decision, not a
style preference.
Pairs with Module 11 — RAII and Smart Pointers
The three pointers¶
std::unique_ptr<T> |
std::shared_ptr<T> |
std::weak_ptr<T> |
|
|---|---|---|---|
| Ownership | Exclusive | Shared | None |
| Copyable | No | Yes | Yes |
| Movable | Yes | Yes | Yes |
| Cost | No larger than a raw pointer | A pointer plus a control block with reference counts | Same as shared_ptr's control block size |
| Destroyed when | The owner goes out of scope | The last owner goes out of scope | Never — it only observes |
| Makes it into a derived destructor? | Yes, if the base's destructor is virtual | Yes, same rule | n/a |
auto one = std::make_unique<Widget>(arg);
auto many = std::make_shared<Widget>(arg);
one->doSomething(); // same syntax as a raw pointer
Widget& ref = *one; // and references, on request
auto moved = std::move(one); // ownership transfer; "one" is now empty
Choosing an ownership type¶
| You mean | Use |
|---|---|
| "I own it, alone, and my lifetime includes it" | A value member |
| "I own it, alone, but it must outlive me or be optional" | std::unique_ptr |
| "We own it together, and nobody can say who outlives whom" | std::shared_ptr — and read the warning below |
| "I only need to look at it, and I promise it outlives me" | T& |
| "I only need to look at it, and it may be absent or moved away" | T* |
| "I only need to look at it, and it may be destroyed while I wait" | std::weak_ptr |
shared_ptr is not the safe default — it is the last resort. It means no single object can
answer "when should this be destroyed?". In most designs the answer is knowable, and
unique_ptr states it. Reach for shared_ptr when observers outlive the natural owner and no
reliable notification exists.
Creating them¶
auto a = std::make_unique<Widget>(1); // preferred
auto b = std::make_shared<Widget>(1); // preferred
std::unique_ptr<Widget> c(new Widget(1)); // works, but see the note
| Rule | Why |
|---|---|
Use make_unique/make_shared |
No new in your code, exception-safe, and (for shared_ptr) one allocation instead of two |
Never construct two shared_ptrs from one raw pointer |
Two control blocks, two owners, one double free: auto p = w.get(); std::shared_ptr<T> a(p), b(p); |
Prefer make_unique over new even in a constructor's member list |
A member initialiser that throws cannot leak what make_unique already owns |
Passing and returning¶
| Signature | Meaning |
|---|---|
void f(Widget& w) |
"I use it and it must exist" |
void f(const Widget& w) |
"I read it, I do not copy it" |
void f(Widget* w) |
"It may be absent; I do not own it" |
void f(std::unique_ptr<Widget> w) |
"I take ownership" |
std::unique_ptr<Widget> make() |
"You now own this" |
Widget& owner() |
"Here is something I keep owning" |
Do not write f(const std::unique_ptr<Widget>&) unless you genuinely need access to the
ownership, rather than to the widget. It couples every caller to the ownership model.
Custom deleters, and non-memory resources¶
RAII is not only about memory. A file handle, a socket, a mutex, a C API pointer — all are resources with a release function.
struct FileCloser { void operator()(std::FILE* f) const { std::fclose(f); } };
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
FilePtr openLog(const std::string& path)
{
return FilePtr(std::fopen(path.c_str(), "w")); // closed automatically, however we leave
}
A unique_ptr with a custom deleter is the general form of "clean this up when I go out of
scope", including on the exception path. See Exception Handling for why that
matters.
Cycles: the one bug shared_ptr brings with it¶
Two objects holding each other's shared_ptr keep each other alive forever — neither count ever
reaches zero.
struct Node
{
std::shared_ptr<Node> next;
std::weak_ptr<Node> back; // the back edge does not own
};
Rule: in a parent↔child pair where each needs the other, one direction is shared_ptr and the
other is weak_ptr. Always call expired() or use lock() before dereferencing a weak_ptr:
A cycle is not a compile-time or run-time error. The program ends, and the objects are simply never destroyed — visible only with a construction/destruction counter or a leak checker.
Raw pointers are not obsolete¶
A raw pointer is the right answer when all of these hold:
- The pointer does not own anything.
- The pointed-to object is guaranteed to outlive the pointer's use.
- The pointer cannot be null at the point of use, or you check for null at the point of use.
Name the role in the variable or the parameter (const Widget* observer) so nobody has to guess,
and never delete through a pointer you do not own.
Checklist¶
- [ ] Does every owning pointer say so in its type?
- [ ] Is
shared_ptrused only where the lifetime genuinely cannot be attributed? - [ ] Is every
newgone in favour ofmake_unique/make_shared? - [ ] Is there any parent/child pair of
shared_ptrs — a cycle? - [ ] Are pointers into a container invalidated by a
push_backor an erase? - [ ] Are polymorphic objects deleted through a base with a virtual destructor?
- [ ] Is there a single owner for every resource, expressible in one sentence?
Related: Rule of Zero, Three and Five · Exception Handling · Common OOP Bugs · Module 11 — RAII and Smart Pointers