C++ Smart Pointers Cheat Sheet
Covers unique_ptr, shared_ptr, and weak_ptr semantics, ownership models, custom deleters, and how to avoid reference-cycle memory leaks.
std::unique_ptr
Exclusive ownership smart pointer with zero overhead.
#include <memory>std::unique_ptr<int> p = std::make_unique<int>(42); // preferred over newstd::cout << *p;std::unique_ptr<int> p2 = std::move(p); // ownership transferred, p is now null// std::unique_ptr<int> p3 = p2; // compile error: copying is disabledvoid takeOwnership(std::unique_ptr<int> ptr) { /* ... */ }takeOwnership(std::move(p2)); // must move, cannot copy// custom deleterstd::unique_ptr<FILE, decltype(&fclose)> file(fopen("f.txt", "r"), &fclose);
std::weak_ptr
Non-owning observer of a shared_ptr that breaks reference cycles.
std::shared_ptr<int> sp = std::make_shared<int>(99);std::weak_ptr<int> wp = sp; // does not increase use_countif (auto locked = wp.lock()) { // returns a shared_ptr, or nullptr if expired std::cout << *locked;} else { std::cout << "expired";}std::cout << wp.expired(); // true if the managed object was already destroyed
Avoiding Cyclic References
Break shared_ptr reference cycles that leak memory.
struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // weak_ptr breaks the cycle};auto a = std::make_shared<Node>();auto b = std::make_shared<Node>();a->next = b;b->prev = a; // if this were shared_ptr, a and b would leak forever
Smart Pointer Comparison
When to reach for each smart pointer type.
- unique_ptr- Sole owner, move-only, no runtime overhead versus a raw pointer; the default choice.
- shared_ptr- Multiple owners via atomic reference counting; has overhead from the control block.
- weak_ptr- Non-owning reference to a shared_ptr-managed object; used to break cycles or observe without extending lifetime.
- make_unique / make_shared- Preferred factory functions; exception-safe and, for shared_ptr, allocate object + control block in one block.
- Raw pointer- Still fine for non-owning, non-nullable observation where lifetime is guaranteed by the caller.
unique_ptr with Arrays and Polymorphic Deletion
Handle array ownership and ensure base-class destructors run correctly.
// array specialization calls delete[] automaticallystd::unique_ptr<int[]> arr = std::make_unique<int[]>(10);arr[3] = 42;// storing a derived object through a unique_ptr<Base> REQUIRES a virtual destructor,// otherwise only Base::~Base runs and Derived's resources leakstruct Base { virtual ~Base() = default; };struct Derived : Base { std::vector<int> data{1, 2, 3}; };std::unique_ptr<Base> p = std::make_unique<Derived>();p.reset(); // safe: virtual dtor ensures ~Derived runs first// covariant return with unique_ptr (C++14+)struct Cloneable { virtual std::unique_ptr<Cloneable> clone() const = 0; virtual ~Cloneable() = default; };
Control Block Layout & Cost
Understand what make_shared actually allocates and why it matters for weak_ptr lifetime.
// make_shared allocates ONE block: [control block | T object] contiguouslyauto sp = std::make_shared<int>(5);// shared_ptr(new T(...)) allocates TWO separate blocks: object, then control blockstd::shared_ptr<int> sp2(new int(5)); // extra allocation + worse cache locality// gotcha: with make_shared, the T's memory is NOT freed until the control block// itself is freed - i.e. until the LAST weak_ptr also disappears, not just the// last shared_ptr. A long-lived weak_ptr can keep a large object's memory pinned.std::weak_ptr<int> wp = sp;sp.reset(); // int's destructor runs now...// ...but the underlying storage for the control block + int stays allocated// until wp itself is destroyed, because they share one allocation.
Advanced Pitfalls & Idioms
Issues that surface once smart pointers are used beyond toy examples.
- std::shared_ptr Thread Safety- The control block's refcount is atomic (safe to copy/destroy from multiple threads), but the pointee itself is NOT synchronized - concurrent writes to *sp still need a mutex.
- Pimpl with unique_ptr- A class holding std::unique_ptr<Impl> to an incomplete type must declare (not =default in the header) its destructor, or the implicit inline destructor fails to compile against the incomplete type.
- shared_ptr<void>- Type-erases the deleter while keeping it callable; used to store heterogeneous owned resources, e.g. in a cache keyed by name.
- std::enable_shared_from_this pitfall- Calling shared_from_this() on an object not already owned by a shared_ptr throws std::bad_weak_ptr; never call it from a constructor.
- owner_before- Ordering comparator on shared_ptr/weak_ptr based on control-block identity rather than pointer value; needed to put weak_ptrs in an ordered set/map.
- std::atomic<std::shared_ptr<T>> (C++20)- Standard atomic specialization for safely swapping a shared_ptr itself across threads, replacing the deprecated std::atomic_load/store free functions.
Never construct two independent shared_ptrs from the same raw pointer - each gets its own control block and the object gets double-deleted; always copy an existing shared_ptr instead of wrapping the same raw pointer twice.