08 Exception Handling & RAII

Overview: Exception Safety and Lifetime-bound Cleanup

C++ uses Stack Unwinding and RAII to handle errors. By tying system resources to the lifetime of stack objects, you can build exception-safe programs that prevent memory leaks.


1. Exception Handling & Stack Unwinding

C++ uses try, catch, and throw to separate error handling from business logic.

try {
    throw std::runtime_error("Device error");
} catch (const std::exception& ex) {
    std::cout << ex.what(); // Catch by const reference
}

The Stack Unwinding Process:

When an exception is thrown:

  1. The CPU immediately halts execution of the current function.
  2. The runtime walks backwards up the call stack (unwinding) to locate a matching catch block.
  3. During this search, all local stack-allocated variables in the exited stack frames are destructed.
  4. If no matching handler is found, the program terminates immediately via std::terminate.

Best Practices:

  • Never throw on heap: Do not throw pointers (throw new MyException()). This requires manual memory management at the catch site, risking leaks. Throw by value and catch by const reference to prevent object slicing.
  • Never throw in destructors: If an exception is thrown while another exception is unwinding the stack, the runtime terminates immediately. Keep destructors noexcept.

2. The RAII Paradigm

Resource Acquisition Is Initialization (RAII) is C++‘s most important design pattern. It links the allocation and release of resources to the lifecycle of a stack-allocated object.

Stack Object Initialized (Constructor) ---> Resource Acquired (file, memory, lock)
... Object performs work ...
Stack Object Leaves Scope (Destructor) ---> Resource Released (closed, deleted, unlocked)

Why RAII is preferred over C-style checks:

Consider error cleanup in C, which requires nested if statements or goto blocks:

FILE* f = fopen("data.txt", "r");
if (!f) return error_code;
if (!doStep1(f)) { fclose(f); return error_code; }
if (!doStep2(f)) { fclose(f); return error_code; }
fclose(f);

In C++, std::ifstream uses RAII to close the file handle automatically in its destructor:

void readData() {
    std::ifstream file("data.txt"); // Constructor opens file
    doStep1(file);
    doStep2(file);
} // Destructor automatically closes file, even if an exception is thrown

💻 Conceptual Code Demonstration

#include <iostream>
#include <fstream>
#include <stdexcept>
 
// A custom class implementing RAII for raw heap allocation
class IntegerArrayBuffer {
    int* buffer;
public:
    IntegerArrayBuffer(size_t size) : buffer(new int[size]) {
        std::cout << "Buffer allocated on Heap.\n";
    }
    ~IntegerArrayBuffer() {
        delete[] buffer;
        std::cout << "Buffer automatically deleted from Heap.\n";
    }
};
 
void runCalculations() {
    // Heap allocation wrapped in a stack-bound RAII object
    IntegerArrayBuffer myBuffer(100);
 
    // Trigger an error
    std::cout << "Throwing exception...\n";
    throw std::runtime_error("Calculation failed");
} // myBuffer's destructor would run here during normal exit
 
int main() {
    try {
        runCalculations();
    }
    catch (const std::exception& ex) {
        // During stack unwinding, myBuffer's destructor is run
        // BEFORE execution enters this catch block. No memory is leaked.
        std::cout << "Caught exception: " << ex.what() << '\n';
    }
    return 0;
}