RAII C++ — Double-Close Bug in Payment Gateway
Payment gateway crash with SIGPIPE from RAII move constructor copying fd without clearing source.
- Resource Acquisition Is Initialization: tie resource lifetime to object scope
- Constructor acquires resource (file, memory, lock), destructor releases it
- Destructors run automatically on scope exit — even during exceptions
- Performance: zero-cost abstraction — no GC overhead, deterministic cleanup
- Production insight: a missing move constructor can double-free or leak
- Biggest mistake: assuming destructor runs after exception — it does, so don't let another exception escape from it
Imagine you rent a hotel room and the front desk gives you a keycard. The moment you check out, the keycard is automatically deactivated — you don't have to remember to call anyone. RAII works the same way in C++: the moment an object goes out of scope, its destructor automatically 'checks it out' and frees whatever resource it was holding. You tie the resource's lifetime to an object's lifetime, and C++ handles the rest. No manual cleanup, no forgotten frees, no leaks.
Resource leaks are the most expensive bugs in systems programming. A database connection left open under an early return. A mutex never unlocked because an exception fired mid-function. A heap allocation living past its pointer. These aren't theoretical — they've crashed payment gateways and cost teams weeks of debugging. C++ doesn't have a garbage collector, but it has something better: deterministic destruction.
RAII — Resource Acquisition Is Initialization — solves this class of problems in one elegant move. Acquire a resource inside a constructor, release it inside the destructor. Because C++ guarantees the destructor runs when the object leaves scope — normal flow, early return, or exception — you get automatic, exception-safe cleanup for free.
This isn't a library feature or a keyword. It's a design principle baked into C++ lifetimes. By the end you'll know how to write RAII wrappers from scratch, why the standard library uses this pattern everywhere, what happens at the ABI level during stack unwinding, how to handle move semantics correctly in RAII types, and the production gotchas that even experienced C++ developers step on.
What is RAII in C++?
RAII is a design pattern that binds the lifecycle of a resource to the lifetime of a local object. The constructor acquires the resource, the destructor releases it. Because C++ destructors are deterministic — they fire exactly when the object goes out of scope — you get predictable cleanup without manual close() or free() calls.
Here's a minimal RAII wrapper for a file. Note how the destructor is called automatically, even if the read throws. That's the whole point.
Destructors and Scope: The Guarantee That Makes It Work
The entire RAII pattern rests on one C++ guarantee: the destructor of a local object is called when the object leaves scope — for any reason. Normal return? Destructor fires. Early return from a guard clause? Destructor fires. Exception thrown mid-function? The stack unwinds and every fully-constructed local object's destructor is called. This is what makes RAII exception-safe by default.
Contrast with manual cleanup. A function that opens a file, acquires a lock, and then calls a function that might throw: if you forget to close or unlock in every path, you leak. With RAII, each resource is wrapped, and the destructor fires automatically. The compiler generates the cleanup code for you, woven into the stack unwinding logic.
close(). The close() failed, the error was ignored, the fd was never released.close() calls inside destructors with a try-catch and log, but don't throw.RAII in the Standard Library: unique_ptr, lock_guard & fstream
The C++ standard library is built on RAII. Every resource-managing class you use daily follows this pattern. std::unique_ptr owns a heap-allocated object and deletes it when the unique_ptr goes out of scope. std::lock_guard locks a mutex on construction and unlocks it on destruction — no way to forget the unlock. std::ifstream opens a file in its constructor and closes it in its destructor.
Understanding these classes as RAII wrappers changes how you read code. When you see std::unique_ptr<T> p = std::make_unique<T>(args), you know the heap memory is safe. When a function returns a std::unique_ptr, ownership is transferred cleanly. No raw delete calls, no missing unlocks, no dangling file handles.
- unique_ptr deleter runs when pointer goes out of scope
- lock_guard unlocks the mutex exactly once — even if
lock()is not called - fstream closes file handle in destructor
- shared_ptr uses reference counting: destructor decrements count, deletes at zero
- Never mix raw pointers with RAII wrappers — ownership becomes unclear
Move Semantics and RAII: Correct Ownership Transfer
RAII types own resources exclusively. You cannot copy them (or copying would duplicate the resource handle, leading to double-free). But you must be able to move them — otherwise you can't return an RAII wrapper from a function, or pass it into a container. Move semantics solve this: a move constructor transfers the resource from the source to the destination, leaving the source in a valid-but-empty state (typically with a null handle). The source destructor then does nothing because there's nothing to release.
- Steal the resource handle from the source
- Set the source's handle to the empty/zero state so its destructor doesn't release it
- Mark the move constructor noexcept (important for performance and correct behavior with containers)
- Do the same for move assignment, handling self-assignment
The Rule of Three, Five, and Zero
RAII ties directly to C++'s resource management rules. The Rule of Three says: if you need a custom destructor, copy constructor, or copy assignment, you probably need all three. The Rule of Five extends this to include move constructor and move assignment. The Rule of Zero says: if your class delegates all resource management to RAII members (like unique_ptr or vector), you don't need to write any of these special functions — the defaults work.
Understanding these rules helps you design RAII classes correctly. If you write a destructor but forget copy operations, you get shallow copies leading to double-free. If you write a move constructor but forget move assignment, you get undefined behaviour on assignment. The Rule of Zero is the ideal: let the standard library's RAII classes manage your resources.
- Rule of Three: destructor, copy ctor, copy assignment go together.
- Rule of Five: adds move ctor and move assignment.
- Rule of Zero: if all members are RAII, don't define any special functions.
- Violations cause double-free, leaks, or undefined behaviour.
- Use =default and =delete explicitly to document intent.
Production Gotchas: Exception Safety, Circular References & ABI
Even with RAII, production C++ has traps. Exception safety: if an exception escapes a destructor during stack unwinding, std::terminate is called — your process dies. Always wrap destructor bodies with try-catch and log; never throw. Circular references with shared_ptr: two objects each holding a shared_ptr to the other never reach reference count zero. You get a memory leak that's invisible until the process dies. The fix is weak_ptr for back-pointers.
ABI compatibility: if you expose an RAII type across shared library boundaries, the destructor code must be in a compiled translation unit, not a header, to avoid ODR violations when different compilers link. Also, if the class has virtual functions, the vtable pointer must be the same everywhere.
- Never let exceptions escape destructors during unwinding
- Mark destructors noexcept (even if your code could technically throw — catch internally)
- Use std::terminate_handler to log where the double-throw happened
- Circular shared_ptr: use weak_ptr to break cycles
- For ABI safety, put non-inline destructor definitions in .cpp files
close(), which could fail with EINTR, and the developer threw an exception to signal the error. During an exception from another part of the code, this destructor threw — immediate terminate and process death.RAII with Coroutines and Asynchronous Code
C++20 coroutines introduce a complication: the lifetime of a coroutine frame is controlled by the coroutine handle, not by standard scope. If a RAII object is captured by reference in a coroutine that outlives the object's scope, you get a dangling reference and undefined behaviour. The solution is to move the RAII object into the coroutine by value, or use shared_ptr to extend lifetime. Another gotcha: the promise_type object's destructor runs when the coroutine completes, not when it suspends. If the promise_type itself holds an RAII resource, ensure it's released correctly.
The Double-Close That Took Down a Payment Gateway
close() on the fd, which the moved-to object still thought it owned. The next write on that fd failed.close() in the destructor.- After moving an RAII object, the source must be in a state where its destructor is a no-op.
- Always initialize resource handles to an invalid sentinel (nullptr, -1, etc.) so the destructor can safely check.
- Do not assume move semantics are correct without inspecting the source state after the move.
free(): invalid pointer)Key takeaways
Common mistakes to avoid
7 patternsForgetting to delete copy constructor and copy assignment
MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete;Omitting noexcept on move constructor/assignment
noexcept.Allowing an exception to escape from a destructor during stack unwinding
Circular reference with shared_ptr, no weak_ptr in back edge
Using shared_ptr when unique_ptr suffices
Not using make_unique / make_shared for exception safety
std::make_unique<T>(args) and std::make_shared<T>(args) instead of new directly.Capturing a stack-allocated RAII object by reference in a lambda that outlives the scope
Interview Questions on This Topic
Explain RAII and why it's important in C++. How does it differ from garbage collection?
Frequently Asked Questions
That's C++ Advanced. Mark it forged?
4 min read · try the examples if you haven't