RAII: the idea that changes everything
RAII means Resource Acquisition Is Initialization: acquire a resource in a constructor, release it in the destructor, and let object lifetime drive cleanup. The resource can be heap memory, a file descriptor, a lock, a socket, a mapped region, a temporary directory, or any "must be undone" operation. The key is that cleanup becomes a property of the type, not of every caller remembering every exit path. Once you internalize RAII, most modern C++ design is just variations on this one move.
The reset: in C, cleanup is usually a control-flow problem. In C++, cleanup should be a type-design problem.
How it really works
An object lifetime has a beginning and an end. In C++, constructors run at the beginning,
destructors run at the end, and subobjects are destroyed automatically. For automatic
objects, the end is scope exit. For dynamic objects, the end is delete or the owning RAII
wrapper's destructor. For temporaries, the end is usually the full expression. During stack
unwinding from an exception, destructors for fully constructed automatic objects still run.
That gives C++ a built-in cleanup stack:
| Event | What C++ does |
|---|---|
| Constructor succeeds | object lifetime has begun |
| Constructor throws | already-constructed subobjects are destroyed |
| Scope exits normally | automatic objects are destroyed in reverse construction order |
| Exception leaves scope | automatic objects are destroyed while unwinding |
delete p runs |
destructor runs, then storage is released |
| RAII member dies | its own destructor releases its resource |
RAII types usually follow a simple ownership rule: copy only if duplicate ownership is
valid, move if ownership can transfer, delete copy if two owners would double-release. A
std::lock_guard<std::mutex> is non-copyable because two lock guards cannot both own the
same lock acquisition. A std::unique_ptr<T> is move-only because ownership can transfer
but not duplicate. A std::vector<T> is copyable because copying means creating a distinct
buffer and copying elements.
At machine level, RAII is not a background runtime. The compiler emits calls to destructors
at each scope-exit path it has to preserve. With exceptions enabled, it also emits unwind
metadata and cleanup paths so destructors run when control leaves through a throw. If you
compile the demo with -S, you can see those calls and the extra exception-handling
tables. If you compile with exceptions disabled, RAII still works for normal scope exits,
but throwing through the code is no longer a supported path.
Executable artifact: cleanup on success and failure
The runnable demo lives in
examples/modern-cpp/raii-the-idea-that-changes-everything/. It compares a C function
using a cleanup: label with a C++ function where TempFile and ByteBuffer release
themselves.
cd examples/modern-cpp/raii-the-idea-that-changes-everything
./run.sh
The C side has to thread every future edit through the same cleanup discipline:
FILE *file = tmpfile();
char *buffer = malloc(64);
if (fail_after_open) {
goto cleanup;
}
cleanup:
free(buffer);
if (file != NULL) {
fclose(file);
}
The C++ side moves the protocol into the resource-owning types:
class TempFile {
public:
TempFile() : file_(std::tmpfile()) {
if (file_ == nullptr) {
throw std::runtime_error("tmpfile failed");
}
}
TempFile(const TempFile &) = delete;
TempFile &operator=(const TempFile &) = delete;
~TempFile() {
if (file_ != nullptr) {
std::fclose(file_);
}
}
private:
FILE *file_;
};
run.sh also emits demo.O2.s:
g++ -std=c++20 -O2 -S demo.cpp -o demo.O2.s
Look for the normal path and the cleanup path. The destructor is ordinary generated code,
but the source no longer relies on each caller remembering fclose and free in every
future branch.
Failure modes & trade-offs
- A destructor must not fail outward. Throwing from a destructor during stack unwinding
can terminate the program. Destructors should release, log, set flags, or require an
explicit
close()/commit()step when failure must be reported. - RAII needs clear ownership. Wrapping a borrowed pointer in an owning destructor causes double-free. Storing an owning raw pointer without a destructor causes a leak.
release()is sharp.std::unique_ptr::release()hands you the raw pointer and stops deleting it. After that, you are back in manual-ownership land.- C APIs need adapters. A C handle such as
FILE*,DIR*,pthread_mutex_t, or a file descriptor becomes RAII only after you wrap it in a type with the right destructor. - Process termination can skip destructors.
_Exit,std::quick_exit, abnormal termination, and some signal paths do not run normal stack cleanup. longjmpacross C++ frames is a trap. Do not jump over objects with non-trivial destructors; use C++ exceptions or keep the boundary in C-only code.
In practice
- Acquire in constructors, release in destructors. Avoid two-phase initialization unless the second phase has a strong reason.
- Make invalid states unrepresentable. A constructed RAII object should either own a valid resource or fail to construct.
- Delete copy for unique resources. File descriptors, locks, and raw heap allocations need move semantics or non-copyability, not accidental copy.
- Prefer standard RAII first.
std::vector,std::string,std::unique_ptr,std::lock_guard,std::scoped_lock, andstd::fstreamcover common cases. - Use C cleanup labels inside C, RAII at C++ boundaries. The two styles can coexist, but do not leave ownership ambiguous between them.
Connects to: C vs C++: what you actually gain (and pay) · Smart pointers: unique_ptr, shared_ptr, ownership · Move semantics and value categories · Memory leaks & ownership discipline · Use-after-free & double-free · Stack vs heap · Sanitizers: ASan, UBSan, and TSan
Sources
- cppreference - RAII - compact definition of resource acquisition, destructor release, and exception-safe cleanup. https://en.cppreference.com/w/cpp/language/raii
- cppreference - Destructors - when destructors are invoked, including scope exit,
delete, temporaries, and stack unwinding. https://en.cppreference.com/w/cpp/language/destructor - cppreference - Exceptions - stack unwinding model and exception handling mechanics. https://en.cppreference.com/w/cpp/language/exceptions
- C++ working draft -
[class.dtor]- standard wording for destructors and destruction. https://eel.is/c++draft/class.dtor - C++ working draft -
[except.ctor]- construction, destruction, and unwinding behavior when constructors throw. https://eel.is/c++draft/except.ctor - C++ Core Guidelines - resource-management rules such as managing resources automatically and avoiding naked
new/delete. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines