Skip to content

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:

if (auto strong = node->back.lock()) { strong->touch(); }   // empty shared_ptr if it is gone

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_ptr used only where the lifetime genuinely cannot be attributed?
  • [ ] Is every new gone in favour of make_unique/make_shared?
  • [ ] Is there any parent/child pair of shared_ptrs — a cycle?
  • [ ] Are pointers into a container invalidated by a push_back or 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