Week 24 · Phase 3 — The Architect
The life and death of an object — and the most quietly elegant idea in C++.
Photo · Blake Cheek / Unsplash
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.
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.
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.
The pattern works for any resource:
std::unique_ptr / std::shared_ptr (next week)std::ifstream / std::ofstream (closes itself)std::lock_guard (releases lock at end of scope)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.)
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:
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.
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.
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".
FileHandle example. Use it. Try forgetting the destructor — watch the compiler not warn (yet) but the file stay open if you check with lsof.std::cout message in both the constructor and destructor. Watch the messages appear at scope boundaries — exactly when you'd expect.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 free under the Unsplash license. Candle · Blake Cheek.