Course outline · 0% complete

0/27 lessons0%

Course overview →

RAII: Why Modern C++ Rarely Writes delete

lesson 6-2 · ~11 min · 16/27

The problem with manual delete

Pairing every new with a delete sounds easy until real code intervenes:

void process() {
    int* buf = new int[1000];
    if (loadFailed()) return;   // oops: leaked, delete never runs
    // ... an exception here also skips the delete ...
    delete[] buf;
}

Every early return, break, or exception is a path that can skip the cleanup line. Chasing all paths by hand does not scale.

The C++ answer: RAII

RAII (Resource Acquisition Is Initialization) means: wrap the resource in an object, acquire it in the constructor (code that runs when the object is created), release it in the destructor (code that runs automatically when the object's scope ends). The language guarantees the destructor runs on every exit path.

Unit 7 teaches you to write constructors and destructors yourself. First, meet the standard library's ready-made RAII wrapper for heap memory.

std::unique_ptr

std::unique_ptr<T> (from <memory>) is a pointer-sized object that owns one heap allocation and deletes it in its destructor:

#include <memory>

void process() {
    auto buf = std::make_unique<int[]>(1000);  // heap array, owned
    buf[0] = 42;
    if (loadFailed()) return;   // fine: destructor frees it
}                               // fine: destructor frees it here too
  • std::make_unique<T>(args) allocates and wraps in one step. No raw new in sight.
  • Use it like a pointer: *p, p->member, or buf[i] for the array form.
  • auto asks the compiler to deduce the variable's type from the right-hand side, sparing you writing std::unique_ptr<int[]> out.
  • Unique means exactly one owner: a unique_ptr cannot be copied, only moved to a new owner with std::move.

The modern rule: new/delete are for understanding and for interviews about internals. In code you write, prefer unique_ptr, and even more often prefer containers like std::vector (unit 8), which are RAII wrappers around arrays.

Watching a destructor run at scope exit

The unique_ptr frees its int automatically, and the tiny Probe struct proves destructors run at scope exit, since its message prints even though nothing calls it.

#include <iostream>
#include <memory>

struct Probe {
    ~Probe() { std::cout << "Probe destroyed, scope ended\n"; }  // destructor, unit 7 explains
};

int main() {
    Probe probe;

    auto score = std::make_unique<int>(41);
    *score += 1;
    std::cout << "score: " << *score << "\n";

    std::cout << "main is returning...\n";
    return 0;
}   // score's int freed here, then probe's destructor prints

Output

score: 42
main is returning...
Probe destroyed, scope ended

The ordering of the last two lines is the evidence. return 0; executes, and only after that does the destructor message appear, which shows that cleanup happens after the return value is settled and before the function truly finishes.

Destructors run in reverse order of construction, so score is destroyed first and probe second, even though probe was declared first. That last-in-first-out rule is what makes RAII compose safely, since an object can rely on anything it was built from still being alive during its own destruction.

Why a destructor beats a delete statement

unique_ptr's cleanup is more reliable because its destructor runs automatically on every path out of the scope, including early returns and thrown exceptions.

The pointed-at data still lives on the heap, and nothing about the allocation has changed. What changes is who frees it. The unique_ptr object itself is a stack local, and C++ guarantees that stack objects' destructors run when the scope exits, regardless of how it exits.

manual deleteunique_ptr
normal fall-throughfreesfrees
early return above the deleteleaksfrees
exception thrownleaksfrees
break out of a loopleaksfrees

A manual delete is one line on one path, while a destructor covers all paths. That coverage is the entire insight of RAII, and it is why the same technique is used for files, locks, and sockets rather than only for memory.

Replacing raw new with make_unique

The original version of this program allocated two heap ints with new and never freed either of them. Switching to std::make_unique removes the leak by removing the need for delete at all.

#include <iostream>
#include <memory>

int main() {
    auto a = std::make_unique<int>(10);
    auto b = std::make_unique<int>(20);

    std::cout << "total: " << *a + *b << "\n";
    return 0;
}

Output

total: 30

Dereferencing is unchanged, so *a + *b reads exactly as it did with raw pointers. That is deliberate on the library's part, since a smart pointer is meant to be a drop-in replacement at the point of use.

No delete lines appear anywhere, and that absence is the point. There is no cleanup path to get wrong, no dangling pointer left behind, and no way for a later edit that adds an early return to reintroduce the leak.

auto spares you from writing std::unique_ptr<int> twice per declaration, and make_unique is preferred over std::unique_ptr<int>(new int(10)) because it keeps the raw new out of your code entirely.