Smart Pointers

Smart pointers are objects that own a heap-allocated object and delete it automatically when they're done with it. std::unique_ptr expresses single ownership, std::shared_ptr shared ownership, and std::weak_ptr a non-owning observer of a shared object. With them, you never write delete.

std::unique_ptr

A unique_ptr is the sole owner of its object. When the unique_ptr is destroyed — at the end of its scope, or when its owner is destroyed — it deletes the object:

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

struct Engine {
    std::string model;
    explicit Engine(std::string m) : model(std::move(m)) { std::cout << "start " << model << '\n'; }
    ~Engine() { std::cout << "stop " << model << '\n'; }
};

int main() {
    auto e = std::make_unique<Engine>("V8");      // create
    std::cout << "using " << e->model << '\n';    // use like a pointer
    {
        auto small = std::make_unique<Engine>("electric");
    }                                             // deleted here
    std::cout << "end of main\n";
}                                                 // V8 deleted here
Output
start V8
using V8
start electric
stop electric
end of main
stop V8

Create them with std::make_unique, never with a raw new. A unique_ptr costs nothing extra compared with a raw pointer.

Moving ownership

A unique_ptr can't be copied — there can only be one owner — but ownership can be moved:

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

int main() {
    auto a = std::make_unique<int>(42);
    // auto b = a;                 // error: can't copy a unique_ptr
    auto b = std::move(a);         // ownership moves to b
    std::cout << std::boolalpha << (a == nullptr) << ' ' << *b << '\n';

    std::vector<std::unique_ptr<int>> owners;
    owners.push_back(std::move(b));
    owners.push_back(std::make_unique<int>(7));
    for (const auto& p : owners) std::cout << *p << ' ';
    std::cout << '\n';
}
Output
true 42
42 7

Functions express ownership in their signatures: a parameter of type std::unique_ptr<T> takes ownership; returning one gives ownership to the caller. Functions that only use the object take T& or T*.

Polymorphic objects

The most common reason to heap-allocate in modern C++ is storing objects of different classes behind a base-class pointer (lesson 37):

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

struct Shape {
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

struct Square : Shape {
    double side;
    explicit Square(double s) : side(s) {}
    double area() const override { return side * side; }
};

struct Circle : Shape {
    double r;
    explicit Circle(double radius) : r(radius) {}
    double area() const override { return 3.14159 * r * r; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Square>(2.0));
    shapes.push_back(std::make_unique<Circle>(1.0));
    for (const auto& s : shapes) std::cout << s->area() << '\n';
}
Output
4
3.14159

std::shared_ptr

When several parts of a program need to keep the same object alive, use shared_ptr. It keeps a reference count; the object is deleted when the last shared_ptr to it goes away:

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

struct Texture {
    std::string file;
    explicit Texture(std::string f) : file(std::move(f)) {}
    ~Texture() { std::cout << "unloading " << file << '\n'; }
};

int main() {
    auto grass = std::make_shared<Texture>("grass.png");
    std::cout << "owners: " << grass.use_count() << '\n';
    {
        auto tile1 = grass;        // copying adds an owner
        auto tile2 = grass;
        std::cout << "owners: " << grass.use_count() << '\n';
    }
    std::cout << "owners: " << grass.use_count() << '\n';
    grass.reset();                 // last owner gone: deleted now
    std::cout << "done\n";
}
Output
owners: 1
owners: 3
owners: 1
unloading grass.png
done

Shared ownership has costs — atomic reference counting and harder-to-follow lifetimes — so use unique_ptr unless ownership really is shared.

std::weak_ptr and cycles

If two objects hold shared_ptrs to each other, their counts never reach zero and both leak. A weak_ptr observes a shared object without owning it; you lock() it to get a temporary shared_ptr, which is empty if the object has already gone:

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

int main() {
    std::weak_ptr<int> observer;
    {
        auto data = std::make_shared<int>(99);
        observer = data;
        if (auto p = observer.lock()) std::cout << "still alive: " << *p << '\n';
    }
    if (observer.expired()) std::cout << "object is gone\n";
}
Output
still alive: 99
object is gone

Typical uses: back-pointers from child to parent, caches, and observers.

Guidelines

  • Prefer objects by value and containers; heap-allocate only when you need to.
  • Use std::make_unique by default, std::make_shared when ownership is genuinely shared.
  • Pass T&, const T& or T* to functions that just use an object — not the smart pointer.
  • Raw pointers never own.

Try it yourself

Build a small playlist that owns its songs through unique_ptrs, and move a song from one playlist to another.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
morning: 1 song(s), evening: Coffee Time