Smart Pointers

Smart pointers apply RAII directly to dynamic memory, so ownership of a heap allocation is expressed in the type system itself — unique_ptr for sole ownership, shared_ptr for shared ownership, and weak_ptr for a safe, non-owning glance at an object someone else owns.

Raw pointers in C++ answer one question -- where is this object in memory -- while leaving three much more important questions completely unanswered: who owns this object, who is responsible for deleting it, and is it even still alive right now? Smart pointers are RAII applied to heap memory specifically: they wrap a raw pointer in a class whose destructor deletes the pointed-to object automatically, and by choosing which smart pointer type to use, you make an object's ownership model an explicit, checkable part of the code rather than a convention scattered across comments and documentation. This page covers std::unique_ptr for exclusive ownership, std::shared_ptr for shared, reference-counted ownership, std::weak_ptr for observing an object you don't own (and for breaking reference cycles), and the design discipline of deciding, deliberately, which kind of pointer -- smart or plain -- each part of a program should actually use.

Why Smart Pointers Exist

A raw pointer returned from new has no lifetime of its own -- it's just an address, and nothing about its type says whether the code holding it is responsible for eventually calling delete on it, whether some other part of the program already deleted it, or whether more than one piece of code thinks it owns the same object. Every one of the classic pointer bugs -- memory leaks, double-frees, dangling pointers, use-after-free -- is ultimately a symptom of ownership being implicit instead of explicit.

Exactly like the manual-resource-management problem RAII solves in general, this early return skips the delete entirely, leaking the Widget. Smart pointers close this hole the same way any RAII type does: tie the deletion to an object's destructor, which the compiler guarantees runs on every exit path.

void oldStyle()
{
    Widget* w = new Widget();
    doSomething(w);
    if (someCondition) return;   // LEAK: never reaches delete w;
    delete w;
}

unique_ptr: Exclusive Ownership

std::unique_ptr<T> owns exactly one object of type T, deletes it automatically when the unique_ptr itself is destroyed, and cannot be copied -- only moved. That single restriction (no copying) is what makes exclusive ownership enforceable: at any point in the program, there can be at most one unique_ptr responsible for a given object, so there's never any ambiguity about who deletes it.

std::make_unique<Widget>(...) constructs the Widget and wraps it in a unique_ptr in a single expression, avoiding a naked new anywhere in application code. Prefer make_unique over unique_ptr<T>(new T(...)) directly whenever possible -- it's exception-safer in argument lists with multiple heap allocations, and it's simply less to type.

#include <memory>

struct Widget { int id; };

void useUniquePtr()
{
    std::unique_ptr<Widget> w = std::make_unique<Widget>(Widget{42});
    std::cout << w->id << "\n"; // -> works just like a raw pointer for member access

} // w's destructor runs here automatically, deleting the Widget -- no manual delete needed

Moving unique_ptr Ownership

Because unique_ptr can't be copied, transferring ownership from one unique_ptr to another requires an explicit std::move -- which is exactly the point: the transfer has to be visible in the code, and after the move, the source unique_ptr is left holding nullptr rather than a second reference to the same object.

std::move(a) doesn't copy the Widget or even the unique_ptr's control logic -- it casts a to an rvalue, which unique_ptr's move constructor then uses to steal the raw pointer out of a and leave a holding nullptr. There is still only ever one owner; it has simply changed which variable that owner is.

Returning a local unique_ptr by value is automatically treated as a move (or elided entirely) rather than a copy attempt, so factory functions can return freshly created, uniquely owned objects directly without any manual move syntax at the call site.

std::unique_ptr<Widget> a = std::make_unique<Widget>(Widget{1});
std::unique_ptr<Widget> b = std::move(a);

// a is now nullptr -- ownership has fully transferred to b
if (!a) std::cout << "a is empty\n";
std::cout << b->id << "\n"; // 1
std::unique_ptr<Widget> makeWidget(int id)
{
    auto w = std::make_unique<Widget>(Widget{id});
    return w;   // implicitly moved out -- no explicit std::move needed on a return
}

Custom Deleters

unique_ptr's default behavior is to call delete on its managed pointer, but not every resource that behaves like a pointer should be released with plain delete -- a C API handle might need a specific close function, an array allocated with new[] needs delete[], and a pooled or arena-allocated object might need to be returned to its pool rather than freed at all. unique_ptr accepts a custom deleter as a second template argument for exactly this reason.

The unique_ptr's type now includes FileCloser as its deleter type, so instead of calling delete on the FILE* when it goes out of scope, it calls FileCloser's operator()(FILE*) -- which correctly calls fclose. This turns a C-style resource with a mismatched cleanup function into something with the exact same automatic, exception-safe lifetime as any other unique_ptr.

#include <memory>
#include <cstdio>

struct FileCloser
{
    void operator()(FILE* f) const { if (f) std::fclose(f); }
};

std::unique_ptr<FILE, FileCloser> openFile(const char* path)
{
    return std::unique_ptr<FILE, FileCloser>(std::fopen(path, "r"));
}

shared_ptr: Shared Ownership and Reference Counting

std::shared_ptr<T> allows multiple pointers to jointly own the same object, tracked by an atomically updated reference count: every time a shared_ptr is copied, the count goes up; every time one is destroyed, the count goes down; and the moment the count reaches zero, the managed object is deleted automatically. Unlike unique_ptr, shared_ptr can be freely copied -- copying is, in fact, the normal way to share ownership.

use_count() reports how many shared_ptr instances currently share ownership of the same object. The Widget is only actually deleted when the last shared_ptr referring to it -- here, a, once it goes out of scope at the end of the function -- is destroyed, regardless of how many copies existed and were destroyed before that point.

#include <memory>
#include <iostream>

struct Widget { int id; };

void shareOwnership()
{
    std::shared_ptr<Widget> a = std::make_shared<Widget>(Widget{7});
    std::cout << a.use_count() << "\n"; // 1

    {
        std::shared_ptr<Widget> b = a; // copy: count goes to 2
        std::cout << a.use_count() << "\n"; // 2
    } // b destroyed here: count drops back to 1

    std::cout << a.use_count() << "\n"; // 1
} // a destroyed: count drops to 0, Widget is deleted

make_shared and the Control Block

Every shared_ptr group shares a control block -- a small heap-allocated structure holding the reference count, a weak count (covered next), and the deleter. Constructing a shared_ptr from a raw pointer (std::shared_ptr<Widget>(new Widget())) allocates the Widget and the control block as two separate heap allocations; std::make_shared<Widget>(...) allocates both in a single combined block, which is both faster and more exception-safe.

Beyond the performance difference, make_shared avoids a specific exception-safety trap: passing new Widget() directly as a function argument alongside another argument that could throw creates a window where the raw pointer from new could leak if the other argument's evaluation throws before the shared_ptr constructor runs. make_shared has no such window, because there's no bare new pointer ever exposed outside the smart pointer's own construction.

auto a = std::make_shared<Widget>(Widget{1});         // one allocation: object + control block together
auto b = std::shared_ptr<Widget>(new Widget{2});      // two allocations: object, then control block separately

Cycles: shared_ptr's Weak Point

Reference counting has exactly one structural weakness: if two objects hold shared_ptrs to each other, each one's reference count includes the other's pointer, so neither count ever reaches zero, even after every shared_ptr outside the cycle has gone away. The objects leak permanently -- reachable from each other, but unreachable from anywhere else in the program, and therefore never destroyed.

When createCycle returns, the local variables a and b go out of scope and each Node's count drops by one -- from 2 to 1 -- but the two Nodes still hold shared_ptrs to each other, so each count never reaches zero. Neither destructor ever runs, and 'Node destroyed' is never printed for either object.

#include <memory>

struct Node
{
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "Node destroyed\n"; } // never printed for a cyclic pair below
};

void createCycle()
{
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b; // b's count: 2 (b itself + a->next)
    b->next = a; // a's count: 2 (a itself + b->next)
} // a and b (the local shared_ptrs) are destroyed here, dropping each count to 1 --
  // but 1 is not 0, so neither Node is ever destroyed. Leaked forever.

weak_ptr: Breaking Cycles and Observing Without Owning

std::weak_ptr<T> refers to an object managed by a shared_ptr without contributing to its reference count -- it's a non-owning observer that can check whether the object is still alive and, if so, temporarily obtain a real shared_ptr to it. Replacing one direction of a reference cycle with a weak_ptr breaks the cycle: that side no longer keeps the other object's count above zero.

Because b->prev is a weak_ptr, assigning a to it never increments a's reference count. When noCycle returns, a's count (still just 1, from the local variable a) drops to zero immediately, destroying a; that destroys a's next member, dropping b's count to zero as well, destroying b in turn. Both objects are cleaned up correctly.

lock() is the only safe way to actually use what a weak_ptr refers to: it atomically checks whether the object is still alive and, if so, returns a genuine shared_ptr that keeps it alive for as long as locked exists; if the object has already been destroyed, lock() returns an empty shared_ptr instead of a dangling reference. There is no way to dereference a weak_ptr directly -- it must be locked first.

struct Node
{
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;   // non-owning: doesn't affect the pointed-to Node's ref count
};

void noCycle()
{
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;      // b's count: 2
    b->prev = a;       // a's count still 1 -- weak_ptr assignment doesn't increment it
}   // a's count drops to 0 -> a destroyed; that drops b's count to 0 -> b destroyed too
std::weak_ptr<Node> weakRef = someSharedPtr;

if (std::shared_ptr<Node> locked = weakRef.lock())
{
    // locked is a real, valid shared_ptr -- the object is confirmed alive
    std::cout << "still alive\n";
}
else
{
    std::cout << "already destroyed\n";
}

Ownership vs. Observation: When to Use a Raw Pointer

Smart pointers express ownership; they are not a mandate to eliminate raw pointers from a codebase entirely. A raw pointer (or, in most cases, a reference) remains the right tool for a function parameter or a local variable that only needs to observe or use an object it does not own and whose lifetime it does not need to manage -- passing a Widget* or Widget& into a function that reads it says, clearly, 'this code does not own the Widget and will not delete it,' which is exactly true and exactly as it should be documented in the type.

A function signature taking a smart pointer by value is making an explicit claim about ownership transfer or sharing; a function that merely needs to look at an object should take a raw pointer or reference instead, both to avoid needless reference-count churn (with shared_ptr) and to keep the ownership story in the codebase honest and easy to read from signatures alone.

void printWidget(const Widget* w);        // observes only -- does not take ownership
void printWidget(const Widget& w);        // same intent, non-nullable

void takesOwnership(std::unique_ptr<Widget> w); // explicitly takes ownership
void sharesOwnership(std::shared_ptr<Widget> w); // explicitly shares ownership

When Not to Use Smart Pointers

Smart pointers exist to manage heap-allocated ownership -- they are not needed, and add unnecessary overhead or awkwardness, for objects that don't need dynamic allocation at all. A local Widget w; on the stack, destroyed automatically at the end of its scope, needs no smart pointer whatsoever; wrapping it in a heap-allocated unique_ptr<Widget> just to 'be modern' adds an allocation, an indirection, and no benefit. Similarly, function parameters that only observe an object should take references or raw pointers rather than shared_ptr, to avoid needless atomic reference-count increments and decrements on every call.

processUnnecessary pays for a heap allocation, a deallocation, and a pointer indirection to manage an object whose lifetime is already exactly its enclosing scope -- ordinary automatic storage already provides that for free, with better cache locality and no allocator overhead. Reach for a smart pointer when an object's lifetime genuinely needs to extend beyond, or be shared across, simple lexical scope -- not as a default replacement for every local variable.

// Unnecessary -- Widget doesn't need to live on the heap at all here:
void processUnnecessary()
{
    auto w = std::make_unique<Widget>();
    w->doSomething();
}

// Better -- no heap allocation needed for a value with purely local scope:
void processBetter()
{
    Widget w;
    w.doSomething();
}

Choosing the Right Pointer

As a working default: prefer plain values and references wherever an object's lifetime can be tied to ordinary scope. Reach for unique_ptr as the default smart pointer whenever an object genuinely needs dynamic allocation and has one clear owner -- it has effectively zero overhead over a raw pointer and can always be converted into a shared_ptr later if sharing turns out to be needed. Reach for shared_ptr specifically when ownership must genuinely be shared among multiple parts of a program with no single owner able to outlive the others. Use weak_ptr to observe a shared_ptr-owned object without keeping it alive, and specifically to break any ownership cycle a shared_ptr design would otherwise create. And use a raw pointer or reference, never a smart pointer, purely to observe or pass along an object you don't own.

What is the fundamental difference in ownership model between unique_ptr and shared_ptr?

unique_ptr enforces exclusive ownership -- exactly one unique_ptr can own a given object at a time, enforced by disallowing copying (only moving is allowed). shared_ptr allows shared ownership among multiple pointers simultaneously, tracked by a reference count; the managed object is deleted only when the last shared_ptr referring to it is destroyed.

Why does calling std::make_shared<T>(...) instead of std::shared_ptr<T>(new T(...)) matter beyond just being shorter to write?

make_shared allocates the managed object and its control block (which holds the reference counts and deleter) together in a single heap allocation, which is faster and uses less memory than the two separate allocations the raw-new form requires. It also avoids an exception-safety gap where the raw pointer from new could leak if evaluating another function argument throws before the shared_ptr constructor has a chance to take ownership of it.

How does a shared_ptr reference cycle happen, and what leaks as a result?

It happens when two (or more) objects hold shared_ptrs to each other, directly or through a longer chain, so each object's reference count includes a pointer that only goes away when the other object is destroyed -- which itself can't happen while the first object is still holding a shared_ptr to it. Neither count ever reaches zero, so neither object is ever destroyed, even after everything outside the cycle has released its references; both objects leak for the remaining lifetime of the program.

What does weak_ptr::lock() do, and why can't code just dereference a weak_ptr directly?

lock() atomically checks whether the object a weak_ptr refers to is still alive and, if so, returns a genuine, owning shared_ptr to it (keeping it alive for at least as long as that shared_ptr exists); if the object has already been destroyed, it returns an empty shared_ptr instead. weak_ptr has no dereference operator of its own precisely because the object it refers to might already be gone -- lock() is the only safe way to get a usable, guaranteed-valid pointer out of it.

When is a raw pointer or reference still the right choice over any smart pointer?

When the code only needs to observe or use an object without taking any responsibility for its lifetime -- a function parameter that just reads or calls methods on an object owned elsewhere should take a raw pointer or (preferably, when non-null) a reference, not a smart pointer. Passing a smart pointer there would misleadingly suggest ownership transfer or sharing, and for shared_ptr specifically would add needless atomic reference-count overhead on every call.

What does this program print, in order?

It prints: 1, 1, destroyed 1. std::move(a) transfers ownership of the Widget from a to b, leaving a as nullptr, so (a == nullptr) prints 1 (true). b->id then prints 1, since b now owns the original Widget. At the end of main, b (the sole remaining owner) is destroyed, which destroys the Widget and prints 'destroyed 1' -- a's earlier assignment produced no destruction since a never owned anything by then.

Write a function template makeAndReturn that constructs a unique_ptr<T> from a given value and returns it (ownership transferred to the caller), and demonstrate transferring ownership again from that returned unique_ptr into a std::vector<std::unique_ptr<int>> in main().

makeAndReturn constructs a unique_ptr<T> locally and returns it by value, which is treated as a move (never a copy, since unique_ptr has no copy constructor) out of the function. push_back(makeAndReturn(...)) then moves that temporary unique_ptr directly into the vector's storage. The program prints 0 10 20, one value per owned int, each still uniquely owned -- this time by the vector.

Write a program demonstrating a shared_ptr reference cycle between two Node objects (each holding a shared_ptr to the other) by printing a destructor message, then fix it using weak_ptr for one direction and show the destructor messages now print correctly.

With prev as a weak_ptr, b->prev = a; does not increase a's reference count. When main ends, a's count (still 1, from the local variable a) drops to zero, so a is destroyed -- which destroys a->next, dropping b's count to zero and destroying b in turn. The output is 'end of main', 'Node 1 destroyed', 'Node 2 destroyed'. Had prev instead been a shared_ptr, both counts would have stayed at 1 forever after main returned, and neither destructor message would ever print.

What does this program print, and why does the second lock() attempt behave differently from the first?

It prints 99, then expired. Inside the inner scope, shared is still alive, so weak.lock() succeeds and returns a valid shared_ptr, printing 99. Once the inner scope ends, shared (the only owning shared_ptr) is destroyed, deleting the int; weak still exists but now refers to a destroyed object, so its second lock() call correctly returns an empty shared_ptr instead of a dangling reference, and the else branch prints 'expired'.

#include <memory>
#include <iostream>

struct Widget { int id; ~Widget() { std::cout << "destroyed " << id << "\n"; } };

int main()
{
    std::unique_ptr<Widget> a = std::make_unique<Widget>(Widget{1});
    std::unique_ptr<Widget> b = std::move(a);

    std::cout << (a == nullptr) << "\n";
    std::cout << b->id << "\n";

    return 0;
}
#include <memory>
#include <iostream>

int main()
{
    std::weak_ptr<int> weak;
    {
        auto shared = std::make_shared<int>(99);
        weak = shared;
        if (auto locked = weak.lock()) std::cout << *locked << "\n";
    }
    if (auto locked = weak.lock()) std::cout << *locked << "\n";
    else std::cout << "expired\n";

    return 0;
}