Skip to content
Laviyx Learn

Try

C++ course menu

C++ Smart Pointers: unique_ptr, shared_ptr and weak_ptr

Learn modern C++ smart pointers: std::unique_ptr for single ownership with make_unique, moving ownership, std::shared_ptr with reference counting and make_shared, std::weak_ptr to break cycles, and how to choose the right pointer.

Intermediate10 min readUpdated
On this page

A smart pointer is an object that holds a pointer and owns what it points to: when the smart pointer is destroyed, it frees the memory automatically. Smart pointers (from <memory>) give you dynamic memory without leaks, double deletes or forgotten delete calls.

There are three:

Smart pointerOwnership
std::unique_ptr<T>Exactly one owner
std::shared_ptr<T>Several owners; freed when the last one goes away
std::weak_ptr<T>Observes a shared_ptr without owning it

unique_ptr: one owner#

Create one with std::make_unique. Use it like a normal pointer, with * and ->:

C++
#include <iostream>
#include <memory>
#include <string>

struct Player {
    std::string name;
    int score = 0;
    explicit Player(std::string n) : name(std::move(n)) {
        std::cout << "Player " << name << " created\n";
    }
    ~Player() {
        std::cout << "Player " << name << " destroyed\n";
    }
};

int main() {
    {
        std::unique_ptr<Player> p = std::make_unique<Player>("Asha");
        p->score += 10;
        std::cout << p->name << " has " << p->score << " points\n";
    }   // p goes out of scope: the Player is deleted automatically
    std::cout << "end of main\n";
}
Output
Player Asha created
Asha has 10 points
Player Asha destroyed
end of main

No delete anywhere, and no leak, even if an exception were thrown inside the block. A unique_ptr is as small and fast as a raw pointer.

Moving ownership#

A unique_ptr cannot be copied, because then two owners would both try to delete the object. Ownership can be moved with std::move, leaving the source empty:

C++
#include <iostream>
#include <memory>
#include <utility>

int main() {
    auto a = std::make_unique<int>(99);
    // auto b = a;                 // error: copying is not allowed
    auto b = std::move(a);         // ownership moves to b

    std::cout << std::boolalpha << (a == nullptr) << ' ' << *b << '\n';
}
Output
true 99

Functions use this to express ownership clearly: returning a unique_ptr hands ownership to the caller, and taking one by value takes ownership from the caller. Move Semantics explains std::move in depth.

unique_ptr in containers#

A std::vector<std::unique_ptr<T>> owns a list of heap objects, a very common pattern for collections of polymorphic objects (see Polymorphism):

C++
#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Task {
    std::string title;
};

int main() {
    std::vector<std::unique_ptr<Task>> tasks;
    tasks.push_back(std::make_unique<Task>(Task{"Write report"}));
    tasks.push_back(std::make_unique<Task>(Task{"Review code"}));

    for (const auto& t : tasks) {
        std::cout << t->title << '\n';
    }
}   // the vector destroys every unique_ptr, which deletes every Task
Output
Write report
Review code

shared_ptr: shared ownership#

Sometimes several parts of a program need the same object, and there is no single obvious owner. A std::shared_ptr keeps a reference count: copying it adds an owner, destroying a copy removes one, and the object is deleted when the count reaches zero:

C++
#include <iostream>
#include <memory>
#include <string>

struct Document {
    std::string title;
    explicit Document(std::string t) : title(std::move(t)) {}
    ~Document() { std::cout << "Document closed\n"; }
};

int main() {
    auto doc = std::make_shared<Document>("Plan");
    std::cout << "owners: " << doc.use_count() << '\n';
    {
        std::shared_ptr<Document> editor = doc;
        std::cout << "owners: " << doc.use_count() << '\n';
    }
    std::cout << "owners: " << doc.use_count() << '\n';
}
Output
owners: 1
owners: 2
owners: 1
Document closed

shared_ptr costs more than unique_ptr: it allocates a control block and updates the count (thread-safely) on every copy. Use it only when ownership really is shared.

weak_ptr: observing without owning#

Two objects that hold shared_ptrs to each other form a cycle: neither count ever reaches zero, and both leak. A std::weak_ptr refers to a shared object without owning it. To use it, call lock(), which returns a shared_ptr (empty if the object is already gone):

C++
#include <iostream>
#include <memory>

int main() {
    std::weak_ptr<int> observer;
    {
        auto value = std::make_shared<int>(7);
        observer = value;
        if (auto locked = observer.lock()) {
            std::cout << "still alive: " << *locked << '\n';
        }
    }
    std::cout << std::boolalpha << "expired: " << observer.expired() << '\n';
}
Output
still alive: 7
expired: true

Typical uses are a child pointing back to its parent, caches, and observers.

Which pointer should I use?#

  1. Can it be a plain object or a container (std::vector, std::string)? Do that. No pointer needed.
  2. Does it need to live on the heap with one clear owner? std::unique_ptr.
  3. Is ownership genuinely shared? std::shared_ptr, with std::weak_ptr to break cycles.
  4. Just need to look at an object someone else owns? A reference, or a raw pointer if it can be null.

Always create smart pointers with std::make_unique and std::make_shared rather than new.

Common mistakes#

Practice#

Practice exercise

Write a function std::unique_ptr<std::vector<int>> make_range(int n) that returns a heap-allocated vector containing 1 to n. In main, print the sum of make_range(10).

Show solution
C++
#include <iostream>
#include <memory>
#include <numeric>
#include <vector>

std::unique_ptr<std::vector<int>> make_range(int n) {
    auto v = std::make_unique<std::vector<int>>();
    for (int i = 1; i <= n; ++i) {
        v->push_back(i);
    }
    return v;   // ownership moves to the caller
}

int main() {
    auto range = make_range(10);
    std::cout << std::accumulate(range->begin(), range->end(), 0) << '\n';
}
Output
55

This shows how ownership is returned. In real code, returning the std::vector<int> by value is simpler and just as fast.

Key takeaway#

Finished this lesson?