Smart pointers

C++ · Practical · 15 min

Smart pointers make heap ownership explicit and leak-free; they replace almost every use of new and delete.

Smart pointers (header <memory>) are RAII classes that own a heap object and delete it automatically.

std::unique_ptr<T> is sole ownership. Create it with std::make_unique<T>(args...). It can't be copied, only moved, so ownership transfers are visible in the code: auto b = std::move(a); leaves a null. It has no overhead over a raw pointer, which makes it the default choice. It also handles arrays (std::unique_ptr<int[]>) and custom deleters, for example to fclose a FILE*.

std::shared_ptr<T> is shared ownership with reference counting: each copy increments a count, and the object is deleted when the last shared_ptr goes away. Create it with std::make_shared<T>(args...), which puts the object and its control block in one allocation. The count is updated atomically, so it's safe to copy shared pointers across threads (the pointed-to object itself is not synchronized). Copying costs an atomic increment, so pass shared_ptr by reference or don't pass it at all when the callee doesn't need ownership.

std::weak_ptr<T> observes a shared object without keeping it alive. Call lock() to get a shared_ptr if the object still exists. Use it to break cycles: two objects holding shared_ptrs to each other never reach a count of zero and leak.

Guidelines:

  • Prefer unique_ptr; reach for shared_ptr only when ownership is genuinely shared.
  • Functions that just use an object should take T& or T*, not a smart pointer.
  • Never construct two smart pointers from the same raw pointer: each would delete it.

Example

#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

struct Texture {
    explicit Texture(std::string n) : name{std::move(n)} {}
    ~Texture() { std::cout << "free " << name << '\n'; }
    std::string name;
};

void inspect(const Texture& t) { std::cout << "inspect " << t.name << '\n'; }  // no ownership

int main() {
    auto solo = std::make_unique<Texture>("grass");     // sole owner
    inspect(*solo);
    std::unique_ptr<Texture> owner = std::move(solo);   // ownership moved; solo is null

    auto shared = std::make_shared<Texture>("sky");
    std::vector<std::shared_ptr<Texture>> users{shared, shared};
    std::cout << shared.use_count() << '\n';            // 3

    std::weak_ptr<Texture> watcher = shared;            // doesn't affect the count
    shared.reset();
    users.clear();                                      // last owner gone: "free sky"
    std::cout << watcher.expired() << '\n';             // 1
}                                                       // "free grass"

Remember

  • unique_ptr by default: zero overhead, move-only ownership
  • shared_ptr for genuinely shared ownership; weak_ptr to observe and break cycles
  • Create with make_unique / make_shared; pass T& to functions that don't own

Watch out for

  • Two objects holding shared_ptrs to each other: the cycle keeps both alive forever. Make one side a weak_ptr.

Check yourself

  1. What happens with `auto a = std::make_unique<int>(5); auto b = a;`?
  2. A function only reads a Texture and doesn't care how it's owned. What should it take?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/memory