A single candle burning in the dark. Photo · Blake Cheek / Unsplash
Lit, used, then snuffed. C++ objects work the same way — and the cleanup is automatic.

Every object has two book-end moments: the instant it comes into existence, and the instant it stops existing. C++ gives you a special function for each — a constructor for birth, a destructor for death — and those two functions, combined, are the foundation of an idea so useful that it has its own acronym.

RAII — Resource Acquisition Is Initialisation. Possibly the worst-named good idea in computing. The actual idea: tie the lifetime of a resource to the lifetime of an object. When the object is born, take the resource. When it dies, release it. The compiler enforces the cleanup. You can't forget. You can't double-free. The whole class of bug that haunts C — leaked file handles, unclosed sockets, dangling allocations — disappears.

Constructors

class Account {
public:
    Account(): id_(0), balance_(0) {}                  // default
    Account(int id): id_(id), balance_(0) {}           // one arg
    Account(int id, double b): id_(id), balance_(b) {}   // two args
private:
    int    id_;
    double balance_;
};

Account a;            // default constructor — id 0, balance 0
Account b(42);         // id 42
Account c(42, 100.50);  // id 42, balance 100.50

A class can have multiple constructors with different parameter lists — this is overloading. The compiler picks the right one based on what you pass.

The colon-separated list of name_(value) pairs after the constructor parameters is the initialiser list. It directly initialises the fields, before the body runs. Use it whenever possible — it's required for const and reference fields, and it's faster than assigning in the body for non-trivial types.

A constructor with one argument can be marked explicit, which prevents the compiler from doing surprising implicit conversions. If a constructor takes one argument, almost always make it explicit unless you specifically want implicit conversion to happen.

Destructors

class FileHandle {
public:
    FileHandle(const std::string& path) {
        f_ = fopen(path.c_str(), "r");
        if (!f_) throw std::runtime_error("open failed");
    }
    ~FileHandle() {
        if (f_) fclose(f_);
    }
private:
    FILE* f_;
};

void read_file() {
    FileHandle fh("data.txt");
    // ... use fh ...
    // ... return early? throw an exception? doesn't matter — 
    //     fh's destructor will run, file will be closed, no leak. 
}

The destructor is called automatically when the object goes out of scope — at the closing brace, on early return, or even if an exception fires up through this stack frame. The cleanup runs in every code path, without you doing anything. This is the magic.

Compare to C, where you'd write fopen(), do work, and remember to call fclose() on every return path including error paths — and where forgetting one is a leaked file descriptor that will eventually exhaust the OS's table of open files.

RAII generalised

The pattern works for any resource:

The general rule of modern C++: raw resource handles never escape the class that owns them. If you find yourself writing new without a matching RAII wrapper, pause and ask whether std::unique_ptr would do the job. (It almost always will.)

The Rule of Three (and Five)

If your class manages a resource (raw pointer, file handle, etc.) — meaning you wrote a destructor — you almost certainly also need a custom copy constructor and copy assignment operator. Otherwise the compiler-generated defaults will copy the raw pointer, and now two objects think they own the same resource, and when both destruct you get a double-free.

This is the Rule of Three. C++11 added move operations, making it the Rule of Five:

  1. Destructor
  2. Copy constructor
  3. Copy assignment
  4. Move constructor
  5. Move assignment

Modern C++ has a simpler answer: the Rule of Zero. Use existing RAII types (unique_ptr, vector, string) for your members and you don't need any of these — the compiler-generated defaults work correctly because each member knows how to copy/move/destroy itself. Aim for Rule of Zero. Reach for the others only when you really must.

Order of construction and destruction

Members are constructed in the order they're declared in the class (not the order they appear in the initialiser list — yes, the compiler will warn you if you mismatch them). They're destroyed in reverse order. Base classes are constructed before any members of the derived class, and destructed after.

This matters in practice: a member that depends on another member must be declared after it. The order of fields in your class is part of the contract.

Why this matters for AI

An AI inference engine has hundreds of resources at any moment: GPU memory allocations, CUDA streams, file-mapped model weights, network sockets to a parameter server. Manually managing all of those in C-style "remember to free" code would be a nightmare. With RAII, the cleanup is the type's responsibility — and when a tensor object goes out of scope, its GPU memory is automatically returned to the pool.

This is why modern AI frameworks have surprisingly little explicit memory management code. RAII handles 95% of it; the framework only deals with the remaining tricky cases (cross-thread ownership, lazy deallocation, etc.).

RAII turns "remember to clean up" into "you can't forget".

Try it yourself

What's next

RAII gives you safe management of any resource — including the most common one of all, heap memory. C++ ships with three "smart pointers" that wrap raw new/delete in RAII wrappers. After next week, you'll never write new directly again.

Week 25 is Smart Pointers.

Photo credit

Photo free under the Unsplash license. Candle · Blake Cheek.