Skip to content
C++

Smart Pointers: unique_ptr and shared_ptr

The fundamental problem with raw new and delete is that the programmer must ensure every allocation is freed exactly once — no more, no less. This is surprisingly hard to get right in the presence of multiple return paths, exceptions, or shared ownership between different parts of a program. Smart pointers solve this by wrapping a heap allocation in an object that calls delete automatically in its destructor, applying the RAII principle (Resource Acquisition Is Initialization) to heap memory. C++ provides two main smart pointers in <memory>: std::unique_ptr, which models exclusive single ownership, and std::shared_ptr, which models shared ownership through reference counting.

RAII: tying lifetime to scope

RAII is the core principle that makes C++ resource management predictable: acquire a resource in a constructor, release it in the corresponding destructor. Local objects have their destructors called automatically when they go out of scope — whether that happens because execution reached the closing brace, or because the function returned early, or because an exception was thrown. Smart pointers exploit this guarantee: a smart pointer is a local object whose destructor calls delete on the heap allocation it owns.

#include <memory>

void example() {
    auto p = std::make_unique<int>(42);
    // ... do work ...
    if (some_condition) return;   // p destroyed here → delete called
    // ... more work ...
}                                 // or here → delete called
// Either way, delete is called exactly once.
// No matter which return path is taken, no leak occurs.

std::unique_ptr — exclusive ownership

std::unique_ptr<T> represents a pointer that is the sole owner of its managed object. At any given moment, only one unique_ptr can point to a given heap object. When that unique_ptr is destroyed — when it goes out of scope, or when .reset() is called — the managed object is destroyed and its memory is freed. You interact with the underlying object using the same * and -> operators as a raw pointer.

The recommended way to create a unique_ptr is with std::make_unique<T>(args...). It allocates the object, calls the constructor with the supplied arguments, and returns the smart pointer — all in one step, without any call to new in your code.

#include <memory>
#include <string>
#include <iostream>

struct Widget {
    std::string name;
    Widget(std::string n) : name{std::move(n)} {
        std::cout << "Widget '" << name << "' created\n";
    }
    ~Widget() {
        std::cout << "Widget '" << name << "' destroyed\n";
    }
};

int main() {
    // Preferred: make_unique — no explicit new
    auto w1 = std::make_unique<Widget>("Alpha");

    // Also valid: pass new directly (avoid in modern code)
    std::unique_ptr<Widget> w2{ new Widget("Beta") };

    std::cout << "Name: " << w1->name << "\n";   // -> accesses member
    std::cout << "Name: " << (*w2).name << "\n"; // * dereferences

    // Both destructors run automatically here — no delete needed
}

Output confirms that destructors run at end of scope:

Widget 'Alpha' created
Widget 'Beta' created
Name: Alpha
Name: Beta
Widget 'Beta' destroyed    ← destructor called automatically
Widget 'Alpha' destroyed   ← destructor called automatically

Ownership transfer: moving a unique_ptr

Because unique_ptr enforces single ownership, it deliberately deletes its copy constructor and copy assignment operator — you cannot make a copy of one, because that would mean two owners for one object. If you try, the compiler gives an error. What you can do is transfer ownership from one unique_ptr to another using std::move. After the move, the original pointer is empty (null) and the new pointer is the sole owner.

auto p1 = std::make_unique<Widget>("Alpha");

// auto p2 = p1;               // COMPILER ERROR — copying forbidden
auto p2 = std::move(p1);       // OK — ownership transferred to p2

if (!p1) std::cout << "p1 is now null\n";   // p1 is empty after move
std::cout << p2->name << "\n";              // p2 is the new owner

When passing a unique_ptr to a function that only needs to use the object without taking ownership, pass a const unique_ptr<T>& or simply a raw reference to the underlying type (const T& or T&). Passing by value requires a move, which transfers ownership into the function and leaves the caller with an empty pointer.

std::shared_ptr — shared ownership with reference counting

std::shared_ptr<T> allows multiple pointers to refer to the same heap object at the same time. Internally it maintains a reference count (sometimes called a use count) that tracks how many shared_ptr objects currently point to the managed object. Every time you copy a shared_ptr, the count goes up; every time one is destroyed or reset, the count goes down. When the count reaches zero — meaning no owner remains — the managed object is destroyed and its memory is freed. Unlike unique_ptr, shared_ptr is fully copyable.

Use std::make_shared<T>(args...) to create a shared_ptr. It allocates both the object and the reference-count block in a single allocation, making it more efficient than constructing with new directly.

#include <memory>

auto p1 = std::make_shared<Widget>("Alpha");
std::cout << "use count: " << p1.use_count() << "\n";   // 1

auto p2 = p1;   // copy — both point to the same Widget
std::cout << "use count: " << p1.use_count() << "\n";   // 2

auto p3 = p1;
auto p4 = p1;
std::cout << "use count: " << p1.use_count() << "\n";   // 4

// No new Widget objects were created — just more pointers to the same one.
// The Widget is destroyed when p1, p2, p3, and p4 all go out of scope.

The most common use case for shared_ptr is when an object is legitimately owned by multiple parts of your program, and you cannot predict which will be the last one to finish with it. A typical example is a resource (a database connection, a configuration object, a large data structure) that is created once and shared by several subsystems.

Choosing between unique_ptr and shared_ptr

The right default is unique_ptr. It has zero overhead compared to a raw pointer, makes ownership explicit, and prevents accidental sharing. Use shared_ptr only when you genuinely need shared ownership — when multiple independent owners need the object to stay alive as long as any one of them still needs it. The reference counting in shared_ptr is not free: it requires an extra heap allocation for the control block, and increment/decrement must be atomic for thread safety, which adds cost in multithreaded programs.

Use unique_ptr when…

  • There is one clear owner of the object (a function, a class member)
  • The object's lifetime is tied to a single scope or owner
  • You want the lowest possible overhead — unique_ptr adds nothing over a raw pointer

Use shared_ptr when…

  • Multiple independent owners need access to the same object
  • You cannot predict which owner will be the last to release the object
  • You are implementing caches, observers, or event systems where objects are shared
// unique_ptr: one owner, zero overhead
auto config = std::make_unique<Config>("settings.ini");
config->load();
// config freed at end of scope

// shared_ptr: multiple owners, reference-counted
auto shared_log = std::make_shared<Logger>("app.log");
ServerA a{ shared_log };   // copies increase use count
ServerB b{ shared_log };   // each server keeps logger alive
// Logger destroyed only when a, b, AND shared_log all go out of scope

Essential API at a glance

ExpressionWorks onMeaning
make_unique<T>(args)unique_ptrCreate unique_ptr — preferred over new
make_shared<T>(args)shared_ptrCreate shared_ptr — preferred over new
*pbothDereference — get the managed object
p->memberbothArrow — access member of managed object
p.get()bothReturn raw pointer (no ownership transfer)
p.reset()bothRelease managed object; pointer becomes null
p.reset(new T(...))bothReplace managed object with a new one
!p / if (!p)bothCheck if pointer is null (not managing an object)
std::move(p)unique_ptrTransfer ownership; p becomes null after move
p.use_count()shared_ptrReturn current reference count

Key rules to remember

Prefer make_unique and make_shared over new

These factory functions allocate, construct, and wrap in a single expression, without exposing raw new in your code. They are also exception-safe.

unique_ptr means exactly one owner — it cannot be copied, only moved

Trying to copy a unique_ptr is a compile error. Use std::move to transfer ownership. After a move, the original pointer is null and must not be dereferenced.

shared_ptr is copyable — each copy increments the reference count

The managed object lives until the last shared_ptr pointing to it is destroyed or reset. Pass shared_ptr by value when you want to share ownership; by const reference when you just need to use the object.

Default to unique_ptr — reach for shared_ptr only when you need shared ownership

shared_ptr has overhead: an extra heap allocation for the control block and atomic operations for thread-safe reference counting. unique_ptr has zero overhead over a raw pointer.

Do not mix raw new/delete with smart pointers for the same object

If you pass a raw pointer obtained from new directly to a smart pointer constructor, the smart pointer takes ownership. Never call delete on it separately — that would be a double-free.

Sign in to track progress