⚙️ Unit 14 • C++ Systems Engineering

Exception Handling & Stack Unwinding Architecture

Robust runtime fault tolerance and error resilience in C++: try-catch-throw mechanics, multiple catch hierarchies, nested scopes, catch-all ellipsis handlers (...), rethrowing exceptions with object slicing protection, custom exception hierarchies inheriting from std::exception, stack unwinding destruction guarantees, and the noexcept contract.

⚠️ Fault Model: Non-Local Transfer of Control
📦 Type Hierarchy: std::exception & what()
🌪️ Stack Unwinding: Deterministic RAII Reclamation
🛡️ Contract: noexcept Specifier & Safety
01

Introduction to Errors & The Try-Catch-Throw Architecture

In systems engineering, software errors are classified into three distinct categories:

1. Compile-Time Errors

Syntax violations, type mismatches, and template instantiation failures caught entirely by the compiler prior to binary code generation.

2. Logic / Semantic Errors

Flaws in algorithm design (e.g., off-by-one errors) where the program compiles and executes, but produces incorrect calculations.

3. Runtime Exceptions

Anomalous external conditions occurring during execution: out-of-memory errors, lost socket connections, missing files, or integer division by zero.

C++ handles runtime anomalies via a structured triad of keywords that provide a non-local transfer of control from where the problem occurred to where it can be handled:

try block

Encloses code statements that might potentially encounter or throw an exceptional runtime condition.

throw expression

Signals the occurrence of an anomaly by constructing an exception object and handing control to the C++ runtime exception manager.

catch block

Exception handler placed immediately after a try block, binding to a specific exception type to execute remediation logic.

try_catch_basics.cpp
#include <iostream>
#include <stdexcept>

double calculateThroughput(double bytesTransferred, double seconds) {
    if (seconds <= 0.0) {
        // Construct and throw a standard runtime exception
        throw std::invalid_argument("Time duration must be strictly positive!");
    }
    return bytesTransferred / seconds;
}

int main() {
    try {
        std::cout << "Attempting network calculation...\n";
        double throughput = calculateThroughput(1048576.0, 0.0); // Triggers exception!
        std::cout << "Throughput: " << throughput << " B/s\n";   // Skipped!
    }
    catch (const std::invalid_argument& ex) {
        // Caught by reference: Zero copying, polymorphism preserved
        std::cerr << "[Exception Caught] " << ex.what() << "\n";
    }

    std::cout << "Program safely recovered and continuing execution.\n";
    return 0;
}
⚠️ The Catch-by-Const-Reference Rule

Always catch exceptions by const reference (catch (const std::exception& ex)). Catching by value (catch (std::exception ex)) triggers Object Slicing, truncating derived exception classes to base classes and invoking an expensive, redundant copy constructor during an already critical error path!

02

Multiple Catch Blocks, Catch-All (...) & Rethrowing

A single try block can be followed by multiple chained catch blocks to differentiate handling strategies based on error severity:

🚨 The Catch Ordering Invariant: Most-Derived to Most-Base

C++ matches catch blocks sequentially in the order they appear in source code. A catch block for a base class (e.g., std::exception) will match any derived class (e.g., std::out_of_range). Therefore, handlers for derived exception classes must always precede base classes, otherwise the derived handlers become unreachable dead code!

Catch-All Handler: catch (...)

The ellipsis handler acts as the universal safety net, intercepting any exception regardless of its data type (including raw integers or strings thrown by third-party C libraries). It must always be placed as the final catch block.

Rethrowing Exceptions: Bare throw;

When a catch block performs local logging or partial cleanup and wants to propagate the exception up to an outer caller, writing a bare throw; re-throws the exact same active exception object, preserving its original dynamic type.

multiple_catch_and_rethrow.cpp
#include <iostream>
#include <vector>
#include <stdexcept>

void processPacket(int index) {
    std::vector<int> buffer{10, 20, 30};

    try {
        if (index < 0) {
            throw std::invalid_argument("Negative buffer index!");
        }
        // std::vector::at() throws std::out_of_range on invalid index
        std::cout << "Value: " << buffer.at(index) << "\n";
    }
    catch (const std::out_of_range& ex) {
        // 1. Specialized handler for out_of_range (Derived from logic_error)
        std::cerr << "[Local Error] Boundary fault: " << ex.what() << "\n";
        // Bare rethrow: Propagate up to central audit system
        throw; 
    }
    catch (const std::logic_error& ex) {
        // 2. Base handler for general logic errors
        std::cerr << "[Local Error] Logic failure: " << ex.what() << "\n";
    }
    catch (...) {
        // 3. Universal Catch-All fallback
        std::cerr << "[Fatal] Unidentified foreign exception caught!\n";
    }
}

int main() {
    try {
        processPacket(5); // Out of bounds!
    }
    catch (const std::exception& ex) {
        std::cout << "[Main Handler] Intercepted rethrown error: " << ex.what() << "\n";
    }
    return 0;
}
03

Nested try Blocks & Custom Exception Hierarchies

Complex distributed software frequently nests try-catch scopes. If an inner try block fails to match a thrown exception, the C++ runtime automatically propagates the exception outward to the enclosing try block:

Architecting Custom Exceptions via std::exception

Custom domain exceptions should always inherit publicly from std::exception (or specialized standard derivatives like std::runtime_error). The virtual what() method must be overridden with the noexcept contract:

const char* what() const noexcept override;

custom_exception_architecture.cpp
#include <iostream>
#include <exception>
#include <string>

// Custom Systems Exception inheriting from standard exception
class HardwareInterfaceException : public std::exception {
private:
    std::string message;
    int portCode;

public:
    HardwareInterfaceException(std::string msg, int port)
        : message(std::move(msg)), portCode(port) {}

    // Overriding what() with noexcept guarantee
    const char* what() const noexcept override {
        return message.c_str();
    }

    int getPortCode() const noexcept {
        return portCode;
    }
};

void connectDevice(int port) {
    if (port != 8080) {
        throw HardwareInterfaceException("Hardware handshake timeout on interface bus", port);
    }
    std::cout << "Hardware connected successfully.\n";
}

int main() {
    try {
        // Outer try block
        std::cout << "Initializing bus systems...\n";
        
        try {
            // Nested inner try block
            connectDevice(9000); // Throws HardwareInterfaceException
        }
        catch (const std::bad_alloc& ex) {
            std::cerr << "Inner handler caught memory allocation fault.\n";
        }
        // Inner block didn't catch HardwareInterfaceException -> Propagates to Outer block!
    }
    catch (const HardwareInterfaceException& ex) {
        std::cerr << "[Outer Catch] Failed on Port " << ex.getPortCode() 
                  << " -> " << ex.what() << "\n";
    }

    return 0;
}
04

Stack Unwinding Mechanics & The noexcept Contract

When an exception is thrown, the program abandons normal sequential execution and begins Stack Unwinding. The runtime searches up the call stack frame-by-frame until it encounters a matching catch block:

Stack Unwinding Guarantee

As each stack frame is popped from the call stack, the runtime guarantees that all fully constructed local objects in that frame have their destructors automatically invoked in reverse order (LIFO)! This ensures that memory buffers, open files, and mutexes managed via RAII are deterministically freed.

Destructors Must NEVER Throw!

If a destructor throws an exception while stack unwinding is already in progress for another exception, the runtime cannot handle two simultaneous active exceptions. C++ immediately aborts the process by calling std::terminate()! Destructors in C++11 are implicitly noexcept.

Runtime Stack Unwinding Execution Trace:
1. leafFunction(): Throws exception. Execution halts immediately.
2. Frame leafFunction popped → Destructors for local objects resB and resA executed cleanly.
3. Frame callerFunction popped → Destructor for local object dbSession executed cleanly.
4. main(): Matching catch (const std::exception&) located. Stack unwinding halts. Handler executes.
stack_unwinding_trace.cpp
#include <iostream>
#include <string>
#include <stdexcept>

class ManagedResource {
private:
    std::string name;
public:
    ManagedResource(std::string n) : name(std::move(n)) {
        std::cout << "++ [Resource Acquired] " << name << "\n";
    }

    ~ManagedResource() noexcept {
        std::cout << "-- [Resource Released via Destructor] " << name << "\n";
    }
};

void levelTwo() {
    ManagedResource res2("LevelTwo_Buffer");
    throw std::runtime_error("Critical Hardware Sensor Fault!");
    // res2 destructor is guaranteed to run during unwinding!
}

void levelOne() {
    ManagedResource res1("LevelOne_Socket");
    levelTwo();
    // res1 destructor is guaranteed to run during unwinding!
}

int main() {
    try {
        levelOne();
    }
    catch (const std::exception& ex) {
        std::cout << "==> Main caught exception: " << ex.what() << "\n";
    }
    return 0;
}
💡 The C++11 noexcept Specifier

Marking a function void swap() noexcept informs the compiler and standard library that the function guarantees never to throw an exception. This allows STL containers (like std::vector) to use fast move operations instead of expensive copy operations during dynamic capacity resizes.

05

Viva Voce & Engineering Interview Questions

Q1 • Architecture What is Stack Unwinding in C++, and what happens to local variables when it occurs?
Stack Unwinding is the runtime process that occurs between the moment an exception is thrown and the moment control transfers to a matching catch block. As each stack frame is popped off the call stack, the runtime deterministically calls the destructors of all fully constructed local automatic objects in reverse order of their declaration (LIFO). This ensures that classes adhering to the RAII idiom cleanly release heap memory, file handles, and mutex locks without leakage.
Q2 • Critical Safety Why must a destructor never throw an exception in C++?
If a destructor throws an exception while stack unwinding is already occurring due to a previously thrown exception, the C++ runtime environment is faced with two concurrent unhandled exceptions. Because C++ cannot manage two active exceptions simultaneously, it immediately calls std::terminate(), crashing the application unconditionally. For this reason, all destructors in modern C++ (C++11 onward) default to noexcept.
Q3 • Language Rules Why should catch blocks for derived exception classes always precede catch blocks for base exception classes?
C++ matches catch blocks sequentially in top-to-bottom order. Because a base class reference (e.g., const std::exception&) can bind to any derived object (e.g., std::runtime_error), placing the base handler first causes it to intercept all derived exceptions, rendering the specialized derived catch blocks unreachable dead code.
Q4 • Object Slicing Why is catching exceptions by value considered an anti-pattern?
Catching by value (catch (std::exception ex)) triggers Object Slicing: only the base subobject is copied into the handler, stripping away any derived members and overriding methods. It also invokes the copy constructor unnecessarily during an error path. Catching by const std::exception& preserves polymorphism and avoids copying overhead.
Q5 • Syntax Mechanics What is the difference between writing throw; and throw ex; inside a catch block?
Writing bare throw; rethrows the exact active exception object polymorphically without copying it. Writing throw ex; creates a brand-new exception copy from the parameter ex; if ex was caught by a base class type, it slices the object and throws only the base type rather than the original derived exception.
Q6 • Modern C++ What performance benefit does the noexcept specifier provide in C++11?
noexcept allows compilers to emit significantly smaller and faster machine code by skipping the generation of exception unwinding landing pads. Furthermore, STL containers like std::vector check std::is_nothrow_move_constructible; if move constructors are marked noexcept, std::vector uses high-performance memory moves instead of expensive deep copies during vector reallocations.
Q7 • Exception Contracts What is the difference between the Basic, Strong, and Nothrow Exception Safety Guarantees?
Basic Guarantee: Invariants are preserved and no resources are leaked, but the program state may be modified. Strong Guarantee (Commit-or-Rollback): If an operation throws, the program state is rolled back exactly to what it was before the function was called. Nothrow Guarantee (noexcept): The function guarantees it will never throw an exception under any circumstance.
Q8 • Constructor Exceptions What happens if an exception is thrown from inside a constructor?
If an exception is thrown inside a constructor, the object is considered never constructed. Consequently, its own destructor will NOT be called. However, any data members or base class subobjects that were already fully initialized before the throw will have their destructors automatically invoked in reverse order during stack unwinding.