Loading...

Go Back

Next page
Go Back Course Outline

C++ Full Course


Exception Handling in C++

Exception Handling in C++

Exception handling is a crucial mechanism in C++ for dealing with runtime errors and unexpected situations that can disrupt the normal flow of a program. It allows you to gracefully handle these errors, prevent crashes, and potentially recover from them.

Try/Catch/Throw

C++ provides the try, catch, and throw keywords for exception handling:

  • try: A block of code where exceptions might occur is enclosed in a try block.
  • throw: When an exceptional condition is detected within the try block (or in a function called from within the try block), an exception is "thrown" using the throw keyword. The operand of throw is the exception object itself (which can be of any type).
  • catch: One or more catch blocks follow the try block. Each catch block specifies the type of exception it can handle. When an exception is thrown, the program looks for a catch block that can handle an exception of that type (or a compatible type).
#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return a / b;
}

int main() {
    int x = 10;
    int y = 0;
    int result;

    try {
        result = divide(x, y);
        std::cout << "Result of division: " << result << std::endl;
    } catch (const std::runtime_error& error) {
        std::cerr << "Caught an exception: " << error.what() << std::endl;
    }

    std::cout << "Program continues after exception handling." << std::endl;

    return 0;
}
Caught an exception: Division by zero!
Program continues after exception handling.


Exception Propagation

If an exception is thrown within a function and is not caught by a catch block within that function, the exception is propagated back to the caller function. This process continues up the call stack until a suitable catch handler is found. If no handler is found all the way up to the main function, the program will typically terminate (often by calling std::terminate).

#include <iostream>
#include <stdexcept>

void innerFunction() {
    std::cout << "Inner function started." << std::endl;
    throw std::runtime_error("Exception from inner function!");
    std::cout << "Inner function finished." << std::endl; // This line will not be reached
}

void middleFunction() {
    std::cout << "Middle function started." << std::endl;
    innerFunction();
    std::cout << "Middle function finished." << std::endl; // This line will not be reached
}

int main() {
    std::cout << "Main function started." << std::endl;
    try {
        middleFunction();
    } catch (const std::runtime_error& error) {
        std::cerr << "Caught in main: " << error.what() << std::endl;
    }
    std::cout << "Main function finished." << std::endl;
    return 0;
}
Main function started.
Middle function started.
Inner function started.
Caught in main: Exception from inner function!
Main function finished.

Stack Unwinding

When an exception is thrown and propagates up the call stack, the process of destroying all local objects that were created on the stack between the point where the exception was thrown and the point where it is caught is called stack unwinding. During stack unwinding, the destructors of these local objects are automatically called, which is crucial for resource management (e.g., ensuring that dynamically allocated memory is freed).

Exception Safety

Exception safety refers to writing code that behaves correctly even in the presence of exceptions. There are different levels of exception safety:

  • No guarantee: The program might be in an invalid or unusable state after an exception.
  • Basic guarantee: If an exception is thrown, all of the object's invariants remain valid. No resources are leaked. However, the object's state might have changed.
  • Strong guarantee: If an exception is thrown, the operation has no effect. The program state remains as if the operation had never been attempted. This often involves a "commit or rollback" mechanism.
  • No-throw guarantee: The operation will never throw an exception. This can be indicated using the noexcept specifier (introduced in C++11).

noexcept Specifier (C++11+)

The noexcept specifier can be added to function declarations to indicate whether a function is guaranteed not to throw any exceptions. This can help the compiler perform optimizations and can also affect the behavior of stack unwinding.

#include <iostream>
#include <stdexcept>
#include <vector>

void mightThrow() {
    throw std::runtime_error("This function might throw.");
}

void doesNotThrow() noexcept {
    std::cout << "This function does not throw." << std::endl;
}

int main() {
    try {
        mightThrow();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught exception from mightThrow: " << e.what() << std::endl;
    }

    doesNotThrow();

    // std::vector<int> v;
    // v.push_back(5); // push_back might throw if allocation fails, but std::vector tries to provide strong exception safety

    return 0;
}
Caught exception from mightThrow: This function might throw.
This function does not throw.


RAII for Exceptions

The Resource Acquisition Is Initialization (RAII) principle is particularly important in the context of exception handling. By managing resources (like memory, file handles, locks) through objects whose lifetime is tied to the scope, RAII ensures that resources are automatically released when an exception causes stack unwinding. Smart pointers are a prime example of RAII that helps with exception safety for dynamically allocated memory.

#include <iostream>
#include <fstream>
#include <memory>
#include <stdexcept>

class FileWrapper {
private:
    std::ofstream file;
    std::string filename;

public:
    FileWrapper(const std::string& fname) : filename(fname), file(fname) {
        if (!file.is_open()) {
            throw std::runtime_error("Could not open file: " + filename);
        }
        std::cout << "File '" << filename << "' opened." << std::endl;
    }

    ~FileWrapper() {
        if (file.is_open()) {
            file.close();
            std::cout << "File '" << filename << "' closed." << std::endl;
        }
    }

    void write(const std::string& data) {
        if (file.is_open()) {
            file << data << std::endl;
        } else {
            throw std::runtime_error("File not open for writing.");
        }
    }
};

void processFile() {
    try {
        FileWrapper writer("output_exception.txt");
        writer.write("First line.");
        writer.write("Second line.");
        // Simulate an exception
        if (true) {
            throw std::runtime_error("Simulated error during processing.");
        }
        writer.write("This line might not be written.");
    } catch (const std::runtime_error& e) {
        std::cerr << "Exception caught in processFile: " << e.what() << std::endl;
    }
    // The FileWrapper object 'writer' will be destroyed when it goes out of scope,
    // automatically closing the file, even if an exception occurred.
}

int main() {
    processFile();
    return 0;
}
File 'output_exception.txt' opened.
Exception caught in processFile: Simulated error during processing.
File 'output_exception.txt' closed.

Custom Exceptions

You can define your own exception classes by inheriting from the standard std::exception class or one of its derived classes (like std::runtime_error, std::logic_error, etc.). This allows you to provide more specific information about the errors that can occur in your program.

#include <iostream>
#include <exception>
#include <string>

class MyCustomError : public std::runtime_error {
public:
    MyCustomError(const std::string& message) : std::runtime_error(message) {}
};

void doSomething(int value) {
    if (value < 0) {
        throw MyCustomError("Value cannot be negative: " + std::to_string(value));
    }
    std::cout << "Processing value: " << value << std::endl;
}

int main() {
    try {
        doSomething(10);
        doSomething(-5);
    } catch (const MyCustomError& error) {
        std::cerr << "Caught custom error: " << error.what() << std::endl;
    } catch (const std::runtime_error& error) {
        std::cerr << "Caught a runtime error: " << error.what() << std::endl;
    } catch (const std::exception& error) {
        std::cerr << "Caught a standard exception: " << error.what() << std::endl;
    } catch (...) {
        std::cerr << "Caught an unknown exception." << std::endl;
    }
    return 0;
}
Processing value: 10
Caught custom error: Value cannot be negative: -5

Effective exception handling is essential for writing robust and reliable C++ programs. By using try, catch, and throw, ensuring exception safety, and defining custom exceptions, you can manage errors gracefully and maintain program stability.

Go Back

Next page