Skip to content
C Codeloom
C++

Modern C++ Smart Pointers: unique_ptr and shared_ptr

Use unique_ptr, shared_ptr, and weak_ptr to model ownership in modern C++ without manual new and delete or memory leaks.

·5 min read · By Yash Kesharwani
Intermediate 9 min read

What you'll learn

  • Model sole ownership with unique_ptr
  • Share ownership safely with shared_ptr
  • Break reference cycles using weak_ptr
  • Prefer make_unique and make_shared
  • Pass smart pointers across function boundaries correctly

Prerequisites

  • Pointers and memory — see /blog/cpp-pointers-and-memory

Smart pointers are the modern C++ answer to manual new/delete. They are class templates that own a heap allocation and free it automatically when they go out of scope. Used consistently, they eliminate leaks, double frees, and most dangling pointer bugs.

unique_ptr — sole ownership

std::unique_ptr<T> owns one heap object. It cannot be copied, only moved. When it dies, the object dies with it.

#include <memory>

auto p = std::make_unique<int>(42);
std::cout << *p;       // 42
// no delete needed

std::make_unique is preferred over std::unique_ptr<int>(new int(42)). It is exception-safe and shorter.

Transferring ownership

A unique_ptr moves but does not copy. This expresses transfer of ownership in the type system.

auto a = std::make_unique<int>(1);
auto b = std::move(a);   // b owns the int; a is now empty
// auto c = a;           // error — no copy

After std::move, the source is non-null only if you reassign it. Always reason about a moved-from unique_ptr as empty.

unique_ptr in function signatures

  • Take std::unique_ptr<T> by value when you want the function to take ownership.
  • Take const T& or T* when you only need to read or use the object.
void store(std::unique_ptr<Widget> w);   // takes ownership
void render(const Widget& w);            // borrow, no ownership

Returning a unique_ptr is the natural way to express “factory returns a fresh allocation.”

std::unique_ptr<Widget> make_widget() {
    return std::make_unique<Widget>();
}

shared_ptr — shared ownership

std::shared_ptr<T> allows multiple owners. It maintains a reference count; when the last shared_ptr to the object dies, the object is freed.

auto a = std::make_shared<int>(7);
auto b = a;                  // both own the int
std::cout << a.use_count();  // 2

std::make_shared is faster than the explicit constructor because it allocates the object and the control block in one block.

When to reach for shared_ptr: when ownership is genuinely shared and you cannot identify a single owner. Often you can — reach for unique_ptr first.

weak_ptr — non-owning observer

A std::weak_ptr<T> watches a shared_ptr without contributing to the count. Call lock() to attempt to obtain a shared_ptr; you get a null one if the object is gone.

std::shared_ptr<int> owner = std::make_shared<int>(10);
std::weak_ptr<int> watcher = owner;

if (auto p = watcher.lock()) {
    std::cout << *p;
} else {
    std::cout << "expired";
}

The main use case: breaking reference cycles. Two shared_ptrs pointing at each other never reach zero and leak. Replace one with weak_ptr to break the cycle.

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node>   prev;   // break the cycle
};

Custom deleters

Smart pointers can free non-heap resources by accepting a deleter.

auto file = std::unique_ptr<FILE, decltype(&std::fclose)>(
    std::fopen("data.txt", "r"), &std::fclose);

if (file) {
    char buf[256];
    std::fgets(buf, sizeof(buf), file.get());
}

When file leaves scope, fclose runs. The same pattern wraps C APIs that have paired create/destroy functions.

Smart pointers in containers

unique_ptr is movable, which is enough for std::vector to store it. This is the modern way to hold polymorphic objects.

#include <vector>
#include <memory>

struct Shape { virtual double area() const = 0; virtual ~Shape() = default; };
struct Circle : Shape { /* ... */ };
struct Square : Shape { /* ... */ };

std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Square>());

for (const auto& s : shapes) std::cout << s->area();

When the vector is destroyed, every shape is freed. No leak, no manual loop.

Avoiding common bugs

Never construct two smart pointers from the same raw pointer:

int* raw = new int(1);
std::unique_ptr<int> a(raw);
std::unique_ptr<int> b(raw); // both will delete — double free

Stay inside make_unique and make_shared and you cannot make this mistake.

When passing to functions that take a raw pointer, use .get():

void use(int* p);
auto p = std::make_unique<int>(1);
use(p.get());        // borrow, do not transfer

The function must not free or take ownership of the pointer.

Performance notes

  • unique_ptr has the same size and overhead as a raw pointer. There is no excuse to skip it for performance.
  • shared_ptr is two pointers wide and pays a small atomic cost on copy/destruction. Fine for most code; avoid in tight inner loops.
  • make_shared does one allocation instead of two — usually faster than the explicit form.

A worked example

A small resource handle hierarchy:

#include <memory>
#include <string>

class Logger {
public:
    explicit Logger(std::string name) : name_(std::move(name)) {}
    void log(const std::string& msg) const {
        std::cout << '[' << name_ << "] " << msg << '\n';
    }
private:
    std::string name_;
};

class Service {
public:
    explicit Service(std::shared_ptr<Logger> log) : log_(std::move(log)) {}
    void run() { log_->log("running"); }
private:
    std::shared_ptr<Logger> log_;
};

int main() {
    auto log = std::make_shared<Logger>("main");
    Service a(log), b(log);
    a.run();
    b.run();
}   // Logger destroyed automatically when last Service is gone

Ownership is shared explicitly; lifetime is automatic.

Where this fits

Smart pointers are the cleanest way to apply the Rule of Zero to heap-owned data. They also fit naturally into STL Containers, letting you store polymorphic objects without leaks.

What is next

Round out modern C++ with auto, range-for, and structured bindings. Then revisit the rest of the standard library with confidence that ownership is handled.

Wrap up

Default to unique_ptr and prove you need shared_ptr before reaching for it. Use make_unique and make_shared always. Pass by raw pointer or reference when borrowing; pass by smart pointer when transferring ownership. Manual delete should be a code smell in your reviews.