Senior 4 min · March 06, 2026

RAII C++ — Double-Close Bug in Payment Gateway

Payment gateway crash with SIGPIPE from RAII move constructor copying fd without clearing source.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

file_raii.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <fstream>
#include <stdexcept>
#include <string>

namespace io::thecodeforge {

class FileGuard {
public:
    explicit FileGuard(const std::string& path)
        : file_(path, std::ios::in) {
        if (!file_.is_open()) {
            throw std::runtime_error("Failed to open: " + path);
        }
    }

    ~FileGuard() {
        if (file_.is_open()) {
            file_.close();
        }
    }

    // No copy — resource ownership can't be duplicated
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;

    // Move transfers ownership
    FileGuard(FileGuard&& other) noexcept
        : file_(std::move(other.file_)) {}

    std::string read_line() {
        std::string line;
        if (!std::getline(file_, line)) {
            throw std::runtime_error("Read failed");
        }
        return line;
    }

private:
    std::ifstream file_;
};

} // namespace io::thecodeforge

// Usage — no manual close, even if read_line throws
void process_config(const std::string& path) {
    auto guard = io::thecodeforge::FileGuard(path);
    auto line = guard.read_line();
    // ... do work ...
    // ~FileGuard runs here
}
Why Copy Is Deleted
RAII wrappers usually delete copy constructors. If you allow copying, you'd have two objects thinking they own the same file handle — double-close disaster. Move semantics let you transfer ownership cleanly.
Production Insight
In a real service, missing a move constructor on an RAII type can cause a double-free or resource leak when a temporary is returned from a function.
Rule: if you delete copy, you must write move (or the compiler will copy via const ref and your resource will be copied, not moved — leading to dangling handles).
Debug: use valgrind or AddressSanitizer; a double-free comes from two destructors both trying to release the same fd.
Key Takeaway
RAII ties resource lifetime to object lifetime.
Destructors are deterministic — they run exactly on scope exit.
Without proper move semantics, RAII types break under return or exception.

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.

scope_guarantee.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <stdexcept>

namespace io::thecodeforge {

class ScopedPrinter {
public:
    explicit ScopedPrinter(const char* msg) : msg_(msg) {
        std::cout << "Acquire: " << msg_ << "\n";
    }
    ~ScopedPrinter() {
        std::cout << "Release: " << msg_ << "\n";
    }
private:
    const char* msg_;
};

void might_throw(bool should) {
    ScopedPrinter p("lock");    // acquires
    ScopedPrinter q("file");   // acquires
    if (should) {
        throw std::runtime_error("something bad");
    }
    // p and q destroyed here (if no throw) or during unwind
}

} // namespace io::thecodeforge

int main() {
    try {
        io::thecodeforge::might_throw(true);
    } catch (...) {
        std::cout << "Caught exception\n";
    }
    // Output:
    // Acquire: lock
    // Acquire: file
    // Release: file
    // Release: lock
    // Caught exception
}
Output
Acquire: lock
Acquire: file
Release: file
Release: lock
Caught exception
Destructor Exception Trap
Never let an exception escape a destructor during stack unwinding. If two destructors throw during the same unwind, std::terminate is called and the process dies. Always catch inside destructors or use noexcept.
Production Insight
I've seen a service silently leak sockets because a destructor swallowed an exception that came from close(). The close() failed, the error was ignored, the fd was never released.
Fix: wrap close() calls inside destructors with a try-catch and log, but don't throw.
Rule: destructors must be noexcept in practice, even if marked noexcept(false).
Key Takeaway
Destructors run on all scope exits — normal, early, and exception.
That's the guarantee that makes RAII exception-safe.
Never let an exception escape a destructor during unwinding.
When Does the Destructor Run?
IfLocal object, normal return or scope exit
UseDestructor runs immediately at the closing brace.
IfLocal object, exception thrown in scope
UseDestructor runs during stack unwinding (if fully constructed).
IfObject is a member of another class (by value)
UseDestructor runs as part of the owning object's destruction in reverse order of construction.
IfObject is dynamically allocated (raw new)
UseDestructor runs only when you call delete. Leaks if you forget.

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.

standard_raii.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <memory>
#include <mutex>
#include <fstream>
#include <iostream>

namespace io::thecodeforge {

// Use unique_ptr for heap allocation
class Config {
public:
    void load() {
        std::lock_guard<std::mutex> guard(mutex_);  // lock automatically
        // ... parse config ...
        // destructor unlocks mutex_
    }
private:
    std::mutex mutex_;
};

// Factory returns unique_ptr — no ownership confusion
std::unique_ptr<Config> create_config(const std::string& path) {
    auto config = std::make_unique<Config>();
    // No raw new, no delete needed
    return config;
}

// Using an fstream in an RAII context
void process_line(const std::string& path) {
    std::ifstream file(path);  // opens in constructor
    if (!file) {
        throw std::runtime_error("cannot open");
    }
    std::string line;
    std::getline(file, line);
    // ~ifstream closes file automatically
}

} // namespace io::thecodeforge
Think of RAII as Scope-Bound Resources
  • 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
Production Insight
A common bug: using a custom deleter with unique_ptr that throws. If the deleter throws, the program terminates. Always keep deleters noexcept.
Another trap: forgetting that shared_ptr's destructor can run on any thread (depending on control block). If the deleter has thread-unsafe code, you get intermittent crashes.
Rule: use make_unique and make_shared — they avoid the separate allocation and exception-safety issues of raw new.
Key Takeaway
Standard library classes are RAII wrappers.
unique_ptr owns heap, lock_guard owns mutex, fstream owns file handle.
Let RAII manage resources — never call delete or close manually.

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.

Writing a correct move constructor is subtle. You must
  • 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
move_semantics.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <utility>
#include <cstring>

namespace io::thecodeforge {

class Buffer {
public:
    explicit Buffer(size_t size)
        : data_(new char[size]), size_(size) {}

    ~Buffer() {
        delete[] data_;
    }

    // Copy = deleted because we own raw memory
    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    // Move constructor: steal the pointer
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;  // empty state
        other.size_ = 0;
    }

    // Move assignment: release current, steal from other
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;               // release our old resource
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

private:
    char* data_;
    size_t size_;
};

// Returning a local Buffer from a function works because of move
Buffer create_buffer(size_t sz) {
    Buffer b(sz);
    // ... fill ...
    return b;  // move, not copy (even in C++11)
}

} // namespace io::thecodeforge
Missing noexcept on Move Operations
If std::vector grows and your class's move constructor is not noexcept, std::vector will fall back to copying (if copyable) or refuse to move. This can cause massive performance degradation and, for move-only types like unique_ptr, compile errors.
Production Insight
A legacy codebase had a custom RAII wrapper that forgot to nullify the source in the move constructor. The source destructor would then free the resource that the new object thought it owned. Result: hard-to-reproduce double-frees in production, only showing up under certain call patterns.
Fix: always set source handle to a sentinel value (nullptr, invalid fd -1, etc.) to make the destructor a no-op.
Rule: after stealing a resource, you must make the source forget it.
Key Takeaway
RAII types are move-only (no copy).
Move constructor must steal and nullify source.
Move must be noexcept for container efficiency.
Do I Need Move Operations?
IfRAII type owns a resource, no copy allowed
UseYou must write move constructor and move assignment, and delete copy.
IfResource is a single data member (e.g., unique_ptr member)
UseDefault move may work if the member itself is movable and your class has no custom destructor/copy. Otherwise write your own.
IfReturning RAII object from function
UseMove must work — the compiler relies on move semantics for RVO and local returns.

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.

Rules in Practice
  • 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 Insight
A common production bug: a class with a raw pointer member adds a destructor to delete it, but forgets the copy constructor. The default copy copies the pointer — both objects delete the same memory. Double-free crash.
Fix: either use unique_ptr (Rule of Zero) or explicitly delete copy operations and manage ownership.
Rule: if you have a raw owning pointer, either wrap it in unique_ptr or follow the Rule of Three/Five rigorously.
Key Takeaway
Rule of Three: custom destructor means you need custom copy.
Rule of Five: add move operations.
Rule of Zero: prefer standard RAII types — write less code, fewer bugs.

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.

gotchas.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <memory>
#include <iostream>

namespace io::thecodeforge {

// Bad: circular shared_ptr
struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "~Node\n"; }
};

void circular_demo() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->next = a;  // circular — destructors never run!
}

// Fix: use weak_ptr for back references
struct Node2 {
    std::shared_ptr<Node2> next;
    std::weak_ptr<Node2> prev;  // weak, not shared
    ~Node2() { std::cout << "~Node2\n"; }
};

void no_circular_demo() {
    auto a = std::make_shared<Node2>();
    auto b = std::make_shared<Node2>();
    a->next = b;
    b->prev = a;  // weak, no cycle
    // both destroyed when original pointers go out of scope
}

// Destructor exception safety
class Risky {
public:
    ~Risky() noexcept(false) {
        // Simulate a failure
        throw std::runtime_error("destructor fail");
    }
};

void risk_demo() {
    try {
        Risky r1;
        Risky r2;  // both destructors will throw on unwind
    } catch (...) {
        // Only one exception escapes; second causes terminate
    }
}

} // namespace io::thecodeforge
The Three Exceptions Rule
  • 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
Production Insight
A chat server had a periodic crash in production. The stack trace always ended in std::terminate. The root cause: a custom RAII socket wrapper that called 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.
Fix: catch all exceptions inside the destructor, log the error, and return silently.
Rule: destructors must be noexcept and idempotent — calling them twice should be safe.
Key Takeaway
Exceptions in destructors kill your process.
Circular shared_ptr leaks memory silently.
RAII types across shared libraries need ABI care.

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.

raii_coroutine.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <coroutine>
#include <memory>
#include <iostream>

namespace io::thecodeforge {

struct Resource {
    ~Resource() { std::cout << "~Resource\n"; }
};

struct Task {
    struct promise_type {
        std::unique_ptr<Resource> res = std::make_unique<Resource>();
        Task get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle<promise_type> handle;
    ~Task() { if (handle) handle.destroy(); }
};

Task example() {
    // Resource is owned by promise_type, destructor runs when coroutine completes
    co_return;
}

} // namespace io::thecodeforge

int main() {
    auto t = io::thecodeforge::example();
    // t's destructor calls handle.destroy(), which destroys promise and thus Resource
}
Output
~Resource
Coroutine Lifetime Trap
If you capture a stack-allocated RAII object by reference in a coroutine, that reference may become dangling when the coroutine suspends and the caller's scope exits. Always move the object into the coroutine by value or use a shared_ptr.
Production Insight
A coroutine that captures a RAII lock by reference can unlock the mutex accidentally when the calling scope exits, while the coroutine later tries to access data protected by that lock — race condition.
Fix: std::move the lock_guard into the coroutine frame or use std::shared_ptr to extend lifetime.
Rule: RAII objects captured in coroutines must have their lifetime extended to the coroutine's lifetime.
Key Takeaway
Coroutine frames outlive the caller's scope.
RAII objects must be moved by value into coroutines.
Never capture stack-allocated RAII objects by reference in a coroutine.
RAII in Coroutines: Ownership Strategy
IfRAII object is used throughout the coroutine, no transfer needed
UseCapture by value (move) into the coroutine frame.
IfRAII object is shared across multiple coroutines or callers
UseUse std::shared_ptr with a custom deleter.
IfRAII object is only needed during a synchronous part
UseKeep it in a nested scope and avoid capturing by reference.
● Production incidentPOST-MORTEMseverity: high

The Double-Close That Took Down a Payment Gateway

Symptom
Intermittent crashes with SIGPIPE or ECONNRESET in the payment processing service, often after a network hiccup caused a reconnection path to be taken.
Assumption
The RAII wrapper was sound — it had copy deleted and move defined. No one checked whether the move constructor actually nullified the source handle.
Root cause
The move constructor copied the file descriptor but left the source's fd unset. When the source went out of scope (after the move), its destructor called close() on the fd, which the moved-to object still thought it owned. The next write on that fd failed.
Fix
Set source.fd_ = -1 in the move constructor and check fd_ >= 0 before calling close() in the destructor.
Key lesson
  • 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.
Production debug guideSymptom → Action when your RAII-managed resources are not being released as expected4 entries
Symptom · 01
Process memory grows monotonically; no obvious spike
Fix
Check for circular shared_ptr references. Use Valgrind's massif or heaptrack to identify retained allocations. Look for classes with shared_ptr members that might form cycles.
Symptom · 02
Double-free crash (SIGABRT, free(): invalid pointer)
Fix
Set a breakpoint in the destructor of the suspected RAII type. Inspect the state of the handle. If two different objects have the same handle value, a copy or move is broken. Enable AddressSanitizer to pinpoint the duplicate free.
Symptom · 03
SIGPIPE or EBADF on I/O operations from a seemingly valid object
Fix
Check if the object's resource handle is still valid. An invalid fd (-1) suggests the object was moved from. Add an assertion in methods that use the handle: assert(handle_ >= 0).
Symptom · 04
No observable crash, but resource limit reached (too many open files, too many threads)
Fix
Use lsof or /proc/<pid>/fd to count open file descriptors. If numbers grow, an RAII wrapper is not releasing handles. The destructor may be missing a close/free call, or an exception is escaping the destructor and skipping cleanup (though C++ would call terminate — more likely a missing destructor path).
★ RAII Debug Commands: Find Resource Leaks FastUse these commands to identify resource management failures in C++ production systems.
Memory leak (growing RSS)
Immediate action
Check for circular shared_ptr usage. If no circular references, run a full heap profiler.
Commands
valgrind --tool=massif --threshold=0.1 your_program
ms_print massif.out.<pid> | head -50
Fix now
Identify retaining allocation site. If it's a shared_ptr cycle, convert at least one link to weak_ptr.
Double-free or use-after-free+
Immediate action
Compile with AddressSanitizer and run again under production-like load.
Commands
g++ -fsanitize=address -g -o prog prog.cpp && ./prog
ASAN_OPTIONS=detect_odr_violation=1 ./prog
Fix now
Examine the backtrace: the first free is legitimate, the second is the double-free. Inspect move/copy constructors of the type mentioned.
Too many open file handles (ulimit -n exceeded)+
Immediate action
List open file descriptors by process and count.
Commands
lsof -p $(pgrep your_service) | wc -l
lsof -p $(pgrep your_service) | grep -E 'socket|pipe|REG' | wc -l
Fix now
If file handles increase over time, the RAII wrapper's destructor is not closing. Check that the destructor calls close() and that the object is going out of scope (not held by some long-lived container).
RAII vs Manual Resource Management
AspectRAIIManual
Cleanup triggerDestructor runs on scope exitExplicit call (close, delete, unlock)
Exception safetyAutomatic — destructor runs during unwindMust try-catch every path; easy to miss
Code readabilityNo explicit cleanup code in business logicScattered cleanup calls obscure intent
Move semanticsOwnership transfer via move constructorNo natural mechanism; copy/move prone to double-free
Double-free riskNear zero if copy deleted and move correctHigh — two paths that both call delete

Key takeaways

1
RAII ties resource lifetime to object scope
cleanup is automatic and deterministic.
2
Destructors run on all scope exits, including exceptions
never let them throw.
3
RAII types must be move-only (delete copy); move operations must be noexcept.
4
Standard library classes are RAII
prefer unique_ptr, lock_guard, fstream over raw resource handling.
5
Circular shared_ptr references cause silent memory leaks
use weak_ptr to break the cycle.
6
Follow the Rule of Zero
let RAII members manage resources, avoid writing special functions.

Common mistakes to avoid

7 patterns
×

Forgetting to delete copy constructor and copy assignment

Symptom
Two objects share the same resource handle. When one goes out of scope it releases the resource, and the other later releases it again — double-free crash.
Fix
Explicitly delete copy operations: MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete;
×

Omitting noexcept on move constructor/assignment

Symptom
Containers like std::vector will not use the move operations (they copy instead, often causing giant reallocations). For move-only types, this causes a compile error.
Fix
Mark move constructor and move assignment as noexcept.
×

Allowing an exception to escape from a destructor during stack unwinding

Symptom
std::terminate is called — the application dies immediately with no clean error handling.
Fix
Wrap destructor body in a try-catch that logs the error and does not rethrow. Mark destructor noexcept.
×

Circular reference with shared_ptr, no weak_ptr in back edge

Symptom
Memory leak that grows over time — objects are never destroyed because reference counts never reach zero. No immediate symptom, but process memory grows until OOM.
Fix
Use std::weak_ptr for back-pointers or any non-owning reference in a cycle.
×

Using shared_ptr when unique_ptr suffices

Symptom
Unnecessary reference counting overhead (atomic increments/decrements) and possibility of accidental cycles. Performance degrades under contention.
Fix
Default to unique_ptr. Only use shared_ptr when ownership is genuinely shared.
×

Not using make_unique / make_shared for exception safety

Symptom
If a constructor throws between allocation and wrapping, the raw pointer is leaked. Also, separate new/delete can cause mismatch.
Fix
Always use 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

Symptom
Use-after-free or double-free when the lambda executes later (e.g., stored in a callback) because the original RAII object's destructor has already run.
Fix
Capture the RAII object by value (move) into the lambda, or use shared_ptr to extend lifetime. If the lambda is asynchronous, ensure the RAII object's lifetime is at least as long as the lambda's last execution.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain RAII and why it's important in C++. How does it differ from garb...
Q02SENIOR
How would you design an RAII wrapper for a database connection that must...
Q03SENIOR
What happens if a destructor throws an exception during stack unwinding ...
Q04SENIOR
Explain the Rule of Five and how it relates to RAII.
Q05SENIOR
What is a custom deleter in unique_ptr and when would you use it?
Q06SENIOR
How would you implement a custom RAII wrapper for a Win32 HANDLE that mu...
Q01 of 06JUNIOR

Explain RAII and why it's important in C++. How does it differ from garbage collection?

ANSWER
RAII stands for Resource Acquisition Is Initialization. It's a pattern where a resource (memory, file handle, mutex, etc.) is acquired in a constructor and released in the destructor. Because C++ destructors are deterministic — they run when the object goes out of scope, even on exceptions — you get automatic cleanup. Garbage collection, by contrast, runs nondeterministically at some point after the object is no longer reachable. RAII gives you predictable timing, which is crucial for system resources like locks or file descriptors.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
What is RAII in simple terms?
02
Is RAII only for memory?
03
Can RAII be used with exceptions?
04
What is the difference between RAII and smart pointers?
05
Why can't I copy an RAII object?
06
What is the performance cost of RAII?
07
Does RAII work with asynchronous code?
08
Can RAII be used with std::optional?
🔥

That's C++ Advanced. Mark it forged?

4 min read · try the examples if you haven't

Previous
Move Semantics in C++
4 / 18 · C++ Advanced
Next
Multithreading in C++