Skip to content
C++
Language
since C++11
Advanced

Master C++ Memory Management with RAII and Smart Pointers

Master ownership semantics, RAII, smart pointers, and custom allocators to write leak-free, exception-safe C++ code.

By the end of this page, you will understand ownership as a compile-time property, express every ownership pattern through the type system using std::unique_ptr, std::shared_ptr, and std::weak_ptr, write custom deleters for non-heap resources, and reason confidently about when manual allocation is still the right tool.

What and Why

C++ gives you direct control over object lifetime. That power comes with a responsibility: every resource you acquire must be released exactly once, even when exceptions fly and control flow branches unexpectedly.

The core insight is ownership: at every point in your program, some entity is responsible for each live object. When ownership is implicit β€” tracked only in the programmer's head β€” it leaks or double-frees. When ownership is encoded in the type system, the compiler enforces it for you.

RAII (Resource Acquisition Is Initialization) is the idiom that makes this concrete: tie a resource's lifetime to a stack object's lifetime. The constructor acquires, the destructor releases. Because destructors run deterministically β€” even during stack unwinding from exceptions β€” the resource cannot escape.

C++11 formalized this with three vocabulary types:

TypeOwnership model
std::unique_ptr<T>Sole owner β€” one pointer, one resource
std::shared_ptr<T>Shared ownership β€” reference-counted
std::weak_ptr<T>Non-owning observer of a shared_ptr

Choosing among them is an ownership design decision, not a performance micro-optimisation.

Step by Step

Unique ownership

std::unique_ptr is a zero-overhead abstraction over a raw pointer. Moving it transfers ownership; copying is deleted.

cpp
#include <memory>
#include <iostream>

struct Buffer {
    explicit Buffer(std::size_t n) : data(new char[n]), size(n) {
        std::cout << "Buffer(" << n << ") acquired\n";
    }
    ~Buffer() {
        delete[] data;
        std::cout << "Buffer released\n";
    }
    char* data;
    std::size_t size;
};

int main() {
    auto buf = std::make_unique<Buffer>(1024); // C++14
    // buf is released automatically when it leaves scope
    // even if an exception is thrown above this line
}

std::make_unique (C++14) is preferred over new directly: it avoids the pitfall where an exception between two new expressions in a function argument list leaks one of them.

Transferring unique ownership

cpp
#include <memory>
#include <vector>

std::unique_ptr<int> produce() {
    return std::make_unique<int>(42); // move-elision or explicit move
}

void consume(std::unique_ptr<int> p) {
    // p owns the int here; released on return
}

int main() {
    auto p = produce();
    consume(std::move(p)); // explicit transfer; p is now null
    // p == nullptr here β€” safe to check, unsafe to dereference
}

Shared ownership

When multiple objects genuinely co-own a resource β€” such as nodes in a graph β€” use std::shared_ptr. A control block holds the reference count alongside (or near) the managed object.

cpp
#include <memory>
#include <iostream>

struct Node {
    int value;
    std::shared_ptr<Node> next;
    explicit Node(int v) : value(v) {}
    ~Node() { std::cout << "~Node(" << value << ")\n"; }
};

int main() {
    auto a = std::make_shared<Node>(1); // use_count == 1
    {
        auto b = a;                     // use_count == 2
        b->next = std::make_shared<Node>(2);
    }                                   // b destroyed; use_count back to 1
    // a and a->next both released here
}

std::make_shared allocates the object and control block in a single allocation β€” more cache-friendly and one fewer call to the allocator than std::shared_ptr<T>(new T(...)).

Breaking cycles with weak_ptr

A cycle of shared_ptrs never reaches zero and leaks. Use std::weak_ptr for back-edges or caches: it observes without contributing to the count.

cpp
#include <memory>
#include <iostream>

struct Parent;
struct Child {
    std::weak_ptr<Parent> parent; // non-owning back-reference
    ~Child() { std::cout << "~Child\n"; }
};

struct Parent {
    std::shared_ptr<Child> child;
    ~Parent() { std::cout << "~Parent\n"; }
};

int main() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();
    p->child = c;
    c->parent = p; // weak β€” no cycle
    // Both destructors run here
}

To use a weak_ptr, call .lock() to obtain a temporary shared_ptr. If the object was already destroyed, .lock() returns nullptr.

Custom deleters

Not all resources are heap memory. File descriptors, GPU handles, and OS locks need custom cleanup.

cpp
#include <memory>
#include <cstdio>

int main() {
    auto closer = [](std::FILE* f) { if (f) std::fclose(f); };
    std::unique_ptr<std::FILE, decltype(closer)> fp(
        std::fopen("data.bin", "rb"), closer
    );
    if (!fp) return 1;
    // file closed automatically, even on early return
}

The deleter becomes part of unique_ptr's type, so there is no runtime overhead for stateless lambdas (empty-base optimisation). For shared_ptr, the deleter is type-erased into the control block, so it does not appear in the type.

Common Patterns

Factory returning unique ownership

Callers receive sole ownership and can choose to retain, move, or wrap it in a shared_ptr themselves.

cpp
#include <memory>

class Widget { /* ... */ };

std::unique_ptr<Widget> make_widget(int config) {
    return std::make_unique<Widget>(/* ... */);
}

Pimpl with unique_ptr

Hide implementation details behind a forward-declared class. The destructor must be defined in the .cpp where Impl is complete.

cpp
// widget.h
#include <memory>
class Widget {
public:
    Widget();
    ~Widget();             // declared here, defined in .cpp
    void render();
private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

// widget.cpp
struct Widget::Impl { int x; };
Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // unique_ptr<Impl> destructor runs here
void Widget::render() { /* use impl_->x */ }

Observer list with weak_ptr

A subject holds weak references to observers. Dead observers are silently skipped without the subject knowing β€” no manual deregistration needed.

cpp
#include <memory>
#include <vector>
#include <algorithm>

struct Observer { virtual void on_event() = 0; virtual ~Observer() = default; };

struct Subject {
    void subscribe(std::shared_ptr<Observer> o) {
        observers_.push_back(o);
    }
    void notify() {
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& w) {
                    if (auto o = w.lock()) { o->on_event(); return false; }
                    return true; // prune expired
                }),
            observers_.end()
        );
    }
    std::vector<std::weak_ptr<Observer>> observers_;
};

What Can Go Wrong

Mixing owning and raw pointers carelessly. Passing a raw T* obtained from unique_ptr::get() to a function that stores it beyond the owner's lifetime is undefined behaviour. Use raw pointers only as non-owning, non-stored "borrow" parameters.

cpp
// Wrong: storing a raw pointer that may dangle
struct Cache {
    int* value; // dangerous if the unique_ptr owner is destroyed
};

// Right: accept a reference for non-owning, short-lived access
void process(const int& value);

Constructing shared_ptr from the same raw pointer twice. Each shared_ptr creates its own control block; both will attempt to delete the object.

cpp
int* raw = new int(5);
std::shared_ptr<int> a(raw);
std::shared_ptr<int> b(raw); // double-free β€” undefined behaviour
// Correct: copy or alias from an existing shared_ptr
auto b2 = a;

Using this inside a class to create a shared_ptr without enable_shared_from_this. The fix is to inherit from std::enable_shared_from_this<T> and call shared_from_this().

cpp
#include <memory>

struct Node : std::enable_shared_from_this<Node> {
    std::shared_ptr<Node> self() { return shared_from_this(); }
};

Forgetting the shared_ptr overhead. Each make_shared call hits the allocator and pays atomic reference-count increments on copy/destroy. In hot paths with millions of small objects, a pool allocator or unique_ptr may be significantly faster.

Quick Reference

NeedTool
Single owner, no sharingstd::unique_ptr<T>
Shared ownershipstd::shared_ptr<T>
Non-owning observer/cachestd::weak_ptr<T>
Non-heap resourceunique_ptr + custom deleter
Hide implementationPimpl with unique_ptr
Cheap allocation of shared objectsstd::make_shared
Safe this in shared contextenable_shared_from_this
Borrow without ownershipRaw pointer or reference parameter

Rule of thumb: prefer unique_ptr by default. Reach for shared_ptr only when ownership genuinely cannot be determined at the call site. Use weak_ptr to break cycles or represent optional, non-owning access.

What's Next

  • Understand the concurrency guarantees behind shared_ptr's atomic reference count in Memory Model.
  • See how allocator-aware containers let you control where memory comes from in Memory Management Reference.
  • Explore how constexpr if lets you write allocator policies that compile away entirely in constexpr if (advanced).
  • Learn how lambda captures interact with ownership β€” capturing shared_ptr by value extends lifetime into async callbacks in Lambda (advanced).