Categories
Computer Science / Information Technology Language: C++

Exception Handling

In this section of the course, we will learn about exception handling in C++. Exception handling is a complex topic, but it is essential for writing robust and reliable C++ programs. 

Overview

What is an exception?

An exception is an event that disrupts the normal flow of execution of a program. Exceptions can be caused by a variety of factors, such as invalid input, hardware errors, or programming errors.

What is exception handling?

When an exception occurs, the program can either handle the exception or terminate. Handling an exception means that the program takes steps to recover from the error and continue execution. Terminating the program means that the program stops executing and returns an error code.

Handling exception

In C++, exceptions are handled using the try-catch block. The try block contains the code that is being executed when an exception occurs. In other words, try block contains a block of code that has a possibility to throw an exception. The catch block contains the code that is executed to handle the exception.

Stack unwinding

Stack unwinding is a process that is used to clean up resources when an exception occurs. When an exception occurs, the C++ runtime system unwinds the stack. This means that the runtime system calls the destructors for all of the objects that were created on the stack.

Defining our own exception classes

We can create our own exception classes to provide more information about the exceptions that our programs throw. Exception classes can be derived from the std::exception class.

The Standard Library Exception Hierarchy

The standard exception class hierarchy provides a set of predefined exception classes that can be used to handle common errors. These exception classes include std::out_of_range, std::invalid_argument, and std::runtime_error.

Best practices

Writing exception-safe code is a complex task. Exception-safe code is code that can handle exceptions without causing data corruption. Writing exception-safe code requires careful planning and design.

There are a few best practices for C++ exception handling. These best practices include:

  • Only throw exceptions for exceptional conditions.
  • Always catch exceptions in the smallest scope possible.
  • Rethrow exceptions that cannot be handled.

By following these best practices, you can write C++ programs that are robust and reliable. Do not get overwhelmed with this initial information. We shall see each of these in the section.

Basic concepts of exception handling

In C++, exception handling is a mechanism used to deal with extraordinary situations that may occur during the execution of a program. It allows us to detect and handle errors or exceptional conditions that would otherwise disrupt the normal flow of the program.

Exception handling is specifically designed for synchronous code, where exceptions are thrown and caught within the same thread of execution. It is not meant to handle asynchronous events or multi-threaded scenarios.

The concept behind exception handling is to define what constitutes an extraordinary situation for our application. The definition of such situations depends on the specific requirements and design of the application. Exceptional situations be any of these to start with:

  • Resource limitations, such as running out of memory or storage
  • Missing resources like a required file that doesn’t exist.
  • Invalid operations
  • Range violations
  • Underflows
  • Overflows
  • Illegal data

The goal of exception handling is to detect when an exceptional situation occurs or is about to occur, and then take appropriate actions to handle it. The course of action may vary depending on the nature of the exception and the application itself. In some cases, it may be possible to recover and continue the program execution. However, in more critical situations, termination of the program may be the only feasible option. Even in such cases, we have control over how the program terminates. We can perform cleanup tasks, close files, save data, and ensure a graceful shutdown.

Writing exception-safe code is an important aspect of C++ programming. It means that our code is designed to handle exceptions properly, ensuring that resources are properly released and no memory leaks occur. Achieving complete exception safety is challenging in C++, but by adopting good coding practices and utilising exception handling mechanisms effectively, we can minimise the impact of exceptions on our programs.

Terminology

Let’s delve into the terminology used in C++ when discussing exception handling. While the terminology shares similarities with other programming languages, the way it functions in C++ differs.

Exception

In C++, an exception refers to an object or primitive type (e.g., int, double, boolean) that signals an error condition. This exception object often contains information about the specific problem that occurred. When a code segment determines that something is wrong, it can throw an exception.

Throwing an exception (raising an exception)

The reason code may throw an exception instead of handling the problem itself is that the code might not know how to handle the issue effectively. It throws an exception with the hope that another part of the program can handle it appropriately. This is where catching an exception comes into play. Another section of the program can contain code that catches the thrown exception and performs the necessary actions.

Catching an exception (handle the exception)

Handling an exception can vary depending on the situation. It could involve displaying an error message, logging the error, and terminating the program if it cannot proceed. This is acceptable if the program cannot continue its execution and a graceful failure is desired. However, in other cases, the exception can be handled, allowing for recovery and continued processing.

Example

Let’s consider an example to illustrate this concept. Suppose we want to dynamically allocate memory, but there is insufficient memory available. The code responsible for memory allocation would throw an exception since it couldn’t allocate more memory. However, it doesn’t know how to handle this situation. In another part of the program, we can catch this exception, clear buffers or caches, release some memory, and then attempt the allocation again.

Keywords

C++ employs three keywords to facilitate exception handling: throw, try, and catch.

throw

The throw keyword is used to throw an exception object or primitive type. It is usually followed by an argument representing the exception being thrown.

try { code that may throw an exception }

The try keyword is followed by a code block enclosed in curly braces. This code block contains the segment that may potentially throw an exception, so it is placed within the try block. If no exception is thrown, the code within the try block executes as usual. However, if an exception is thrown, the remaining code in the block is not executed, and C++ searches for a suitable catch block to handle the thrown exception.

catch (Exception ex) { code to handle the exception }

The catch keyword is used to define a catch block, which is followed by the type of exception object it handles. It also has a code block where the code that handles the exception executes.

Catch blocks only execute if an exception is thrown, and the type of the thrown exception matches the parameter in the catch block. We can write multiple catch handlers that expect different types of exceptions.

Example

To better understand exception handling, let’s explore a simple example in C++ that involves division by zero.

Consider the following code snippet, where we calculate the average by dividing the sum by the total:

// Assume sum and total have been declared and initialised
double average;
if (total != 0) {
    average = sum / total;
    // Use the calculated average
}

In this case, we explicitly check if the total variable is zero before performing the division. By doing so, we prevent the program from crashing or producing undefined results. However, if this code exists within a function and the function is expected to return the average, what should we return if the total is zero? This question becomes more challenging to answer.

To handle such situations using exception handling, we can use a try-catch block. Here’s an example to demonstrate the syntax for exception handling:

try {
    // Assume sum, total, and average have been declared and initialised
    if (total == 0) {
        throw 0;  // Throw an exception of type int
    }

    average = sum / total;
    // Use the calculated average

    // Rest of the code within the try block executes normally

    cout << "Program continues" << endl;
} catch (int &exception) {
    // Handle the exception
    cout << "Error: Division by zero" << endl;

    // Code within the catch block executes

    cout << "Program continues" << endl;
}

In the try block, we write the code that may potentially throw an exception. In this case, if total equals 0, we throw an exception of type int with the value 0. It’s worth noting that throwing objects (instead of primitives) is considered a best practice, but we’ll cover that later.

If the total is not equal to 0, the code within the try block executes normally, the division occurs, and we can use the calculated average. After the try block completes, the control transfers to the last statement that displays “Program continues.” Since no exception was thrown, the catch block is skipped entirely.

However, if the total is equal to 0, the code in the try block encounters the throw statement, which immediately transfers the control to the corresponding catch block. Here, the catch block expects an exception object of the same type (int) as the one we threw. The code within the catch block executes, and after it finishes, the control transfers to the last statement that displays “Program continues” again.

By utilising exception handling, we prevent the divide-by-zero error and handle it in a controlled manner. It’s important to note that while the presented code demonstrates the syntax for exception handling, in practice, it’s more common to use if-else statements to handle such cases unless more complex exception handling is required.

Throwing different exception codes

The next step in exploiting the throw-try-catch trio is to throw different error codes for different scenarios. Here’s an example that demonstrates throwing integer exceptions at different places based on various error scenarios, and then handling those exceptions using a switch statement within the catch block.

#include <iostream>
#include <string>

int divideNumbers(int numerator, int denominator) {
    if (denominator == 0) {
        throw 1;  // Throw exception 1 for divide by zero error
    }

    if (numerator < 0) {
        throw 2;  // Throw exception 2 for negative numerator
    }

    if (denominator < 0) {
        throw 3;  // Throw exception 3 for negative denominator
    }

    return numerator / denominator;
}

int main() {
    int numerator, denominator;

    std::cout << "Enter the numerator: ";
    std::cin >> numerator;

    std::cout << "Enter the denominator: ";
    std::cin >> denominator;

    try {
        int result = divideNumbers(numerator, denominator);
        std::cout << "Result: " << result << std::endl;
    } catch (int exception) {
        switch (exception) {
            case 1:
                std::cout << "Error: Divide by zero!" << std::endl;
                break;
            case 2:
                std::cout << "Error: Negative numerator!" << std::endl;
                break;
            case 3:
                std::cout << "Error: Negative denominator!" << std::endl;
                break;
            default:
                std::cout << "Unknown error occurred!" << std::endl;
                break;
        }
    }

    return 0;
}

In this example, we have a function called divideNumbers that takes a numerator and a denominator as input. It performs division and returns the result. However, it can encounter different error scenarios:

  • If the denominator is 0, it throws an exception with the value 1 to indicate a divide-by-zero error.
  • If the numerator is negative, it throws an exception with the value 2 to indicate a negative numerator.
  • If the denominator is negative, it throws an exception with the value 3 to indicate a negative denominator.

In the main function, we prompt the user to enter the numerator and denominator. We then call the divideNumbers function within a try block. If an exception is thrown during the function call, the corresponding catch block executes.

Inside the catch block, we utilise a switch statement to handle different exception scenarios based on their integer values. If exception 1 is caught, it means a divide-by-zero error occurred, and we display the message “Error: Divide by zero!”. If exception 2 is caught, it means a negative numerator was encountered, and we display the message “Error: Negative numerator!”. If exception 3 is caught, it means a negative denominator was encountered, and we display the message “Error: Negative denominator!”. If none of these exceptions are caught, the default case executes and displays the message “Unknown error occurred!”.

By utilising the switch statement within the catch block, we can handle different exception scenarios separately and provide appropriate error messages or actions for each scenario.

Throwing an exception from a function

Let’s explore a common use case in C++ where exception handling can be applied. We’ll examine a function called calculateAverage that takes the sum of integers (assumed to be pre-calculated) and the total number of elements used to obtain that sum. The goal is to determine the average and return it as a double from this function. Additionally, we want to avoid integer division and ensure that division by zero is handled properly.

double calculateAverage(int sum, int total) {
    if (total == 0) {
        throw 0;  // Throw an exception if total is 0
    }
    
    double average = static_cast<double>(sum) / total;
    return average;
}

In the calculateAverage function, we first check if the total is equal to 0. If it is, we throw an exception of type int with the value 0. Throwing an exception causes the division to be bypassed, and the function terminates. The thrown exception will propagate up the call stack until it finds an appropriate exception handler.

Now, let’s see how we can call this function in a way that handles the exception:

try {
    // Assume sum and total have been declared and initialised
    double average = calculateAverage(sum, total);
    cout << "Average: " << average << endl;
} catch (int &exception) {
    cout << "Error: Division by zero" << endl;
}

cout << "Bye!" << endl;

In this code snippet, we encapsulate the function call to calculateAverage within a try block. We anticipate that the function might throw an exception, and if it does, we handle it in the catch block. If no exception is thrown, the code within the try block executes normally.

If an exception is thrown during the function call (i.e., when total is 0), the remaining code within the try block is skipped, and the program searches for a catch block that can handle the thrown exception. In this case, it finds a catch block that expects an integer exception. The code within the catch block executes, displaying an error message indicating division by zero.

After the catch block completes, the control transfers to the last statement, which displays “Bye!” Whether an exception occurs or not, this statement will always execute.

It’s worth noting that we can have multiple catch blocks to handle different types of exceptions or catch-all handlers that can catch any type of exception. However, in this example, we only handle exceptions of type int.

By utilising exception handling, we can ensure that the division by zero scenario is properly handled and the program continues execution in a controlled manner.

The noexcept keyword

In C++, the noexcept keyword is used to specify that a function will not throw any exceptions. It is part of exception specification, which is a way to indicate the exception-handling behaviour of a function.

By using the noexcept keyword, you declare that a function will not throw any exceptions during its execution. This allows the compiler to perform certain optimizations and make assumptions about the function’s behaviour.

Here are some key points about the noexcept keyword:

  1. Exception Specification: The noexcept keyword is used in the function declaration or definition to indicate that the function is declared to be noexcept. It is placed after the parameter list and before the function body.
  2. No Exceptions: A function marked with noexcept guarantees that it will not throw any exceptions. If an exception is thrown from a noexcept function, the std::terminate() function is called, resulting in the termination of the program.
  3. Exception Propagation: If a noexcept function calls another function that may throw exceptions, the exception will propagate to the caller of the noexcept function unless it is caught within the noexcept function.
  4. Exception Specification Dynamic Type: The noexcept keyword can also take an optional argument in parentheses, such as noexcept(expression), where the expression is evaluated at runtime. If the expression evaluates to true, the function is considered noexcept; otherwise, it is not.

Here’s an example demonstrating the usage of noexcept:

void process() noexcept {
    // Function body
}

void foo() noexcept(true) {
    // Function body
}

void bar() noexcept(2 > 1) {
    // Function body
}

int main() {
    process();  // No exceptions thrown
    foo();      // No exceptions thrown
    bar();      // No exceptions thrown

    try {
        process();  // No exceptions thrown
    } catch (...) {
        // This code will not execute because process() is noexcept
    }

    return 0;
}

In the example, the process() function is declared as noexcept and guarantees not to throw any exceptions. It can be called within a try block, but any exceptions thrown from within the function will not be caught.

The foo() and bar() functions also use the noexcept specifier, with the latter demonstrating the usage of an expression in the noexcept argument.

Using noexcept can help improve code optimization and provide guarantees about exception handling. However, it is important to carefully consider the usage of noexcept and ensure that it accurately reflects the function’s behaviour to avoid unexpected program termination.

noexcept vs throw() vs noexcept()

In C++, noexcept, throw(), and noexcept() are used to specify exception handling behaviour and constraints for functions. Here are the differences between these three:

noexcept

noexcept is a C++11 keyword used to declare that a function will not throw any exceptions. When a function is marked as noexcept, it is a guarantee to callers and the compiler that no exceptions will be thrown from that function. If an exception is thrown from a noexcept function, the std::terminate function is called, terminating the program.

Example: void foo() noexcept;

throw()

throw() is an exception specification used in pre-C++11 code to indicate that a function does not throw any exceptions. It is also known as the dynamic exception specification.

In C++11 and later, it is deprecated and replaced by noexcept. It was less reliable than noexcept because it did not provide compile-time checking for exception safety.

Example: void bar() throw();

noexcept()

noexcept() is a C++11 feature that allows you to conditionally specify exception handling behaviour based on expressions.

The expression inside noexcept() determines whether the function is noexcept or not. If the expression evaluates to true, the function is noexcept. If it evaluates to false, the function is not noexcept.

This feature is useful when you want to conditionally specify exception handling based on runtime conditions.

Example: void baz() noexcept(sizeof(T) > 4);

Handling multiple exceptions

Let’s explore the concept of throwing and catching multiple exceptions in C++. Consider a function called calculateMilesPerGallon, which takes the number of miles and the number of gallons as parameters and performs the division to calculate the result. However, this function can encounter multiple error scenarios: division by zero if the number of gallons is 0 and incorrect results if either miles or gallons is negative. We’ll see how we can handle these situations by throwing different types of exceptions.

double calculateMilesPerGallon(int miles, int gallons) {
    if (gallons == 0) {
        throw 0;  // Throw an exception of type int for divide by zero error
    }
    
    if (miles < 0 || gallons < 0) {
        throw std::string("Negative value error");  // Throw an exception of type std::string for negative values
    }
    
    return static_cast<double>(miles) / gallons;
}

In the calculateMilesPerGallon function, we first check if gallons are equal to 0. If it is, we throw an exception of type int with the value 0 to indicate a divide-by-zero error. Additionally, we check if either miles or gallons is negative. If they are, we throw an exception of type std::string initialised with the message “Negative value error” to indicate the presence of negative values. If none of these error scenarios occur, we perform the division and return the result.

Now let’s see how we can handle these exceptions when calling the function:

try {
    // Assume miles and gallons have been declared and initialised
    double milesPerGallon = calculateMilesPerGallon(miles, gallons);
    cout << "Miles per gallon: " << milesPerGallon << endl;
} catch (int &exception) {
    cout << "Error: Divide by zero" << endl;
} catch (std::string &exception) {
    cout << "Error: " << exception << endl;
} catch (...) {
    cout << "Unknown error occurred" << endl;
}

cout << "Buy!" << endl;

In this code snippet, we encapsulate the function call to calculateMilesPerGallon within a try block. We anticipate that the function might throw exceptions of type int or std::string, and we provide separate catch blocks to handle each exception type. Additionally, we have a catch-all handler denoted by the ellipsis (…) that can catch any type of exception.

If no exception is thrown during the function call, the code within the try block executes normally. The result is assigned to milesPerGallon, displayed, and then the statement “Buy!” is executed.

However, if an exception is thrown, the remaining code within the try block is skipped, and the program searches for an appropriate catch block to handle the thrown exception. If the exception is of type int, the corresponding catch block executes and displays the error message “Error: Divide by zero.” If the exception is of type std::string, the respective catch block executes and displays the error message provided in the exception. If none of the catch blocks match the thrown exception type, the catch-all handler executes, displaying the message “Unknown error occurred.”

By utilising multiple catch blocks and a catch-all handler, we can handle different types of exceptions that may occur within the function and provide appropriate error messages or actions for each scenario.

Stack unwinding

When an exception is thrown in a function and the function does not handle the exception itself, the function terminates and is removed from the call stack. At this point, C++ examines the call stack to identify the function that is now at the top, as it must have called the terminated function. If this calling function has a try block, the catch handlers within that block are checked to find a match for the thrown exception. If a match is found, the corresponding catch block is executed, and the program continues as demonstrated in the previous examples. However, if there is no try block in the calling function or the try block does not contain a matching catch handler, the calling function is also removed from the call stack, and the process of stack unwinding continues. Stack unwinding refers to the sequential removal of functions from the call stack in search of an appropriate exception handler.

Let’s enhance the explanation with a code snippet to illustrate stack unwinding:

#include <iostream>

void innerFunction() {
    std::cout << "Inside innerFunction" << std::endl;
    throw "Exception occurred in innerFunction";  // Throw an exception of type const char*
}

void middleFunction() {
    std::cout << "Inside middleFunction" << std::endl;
    innerFunction();
}

void outerFunction() {
    std::cout << "Inside outerFunction" << std::endl;
    try {
        middleFunction();
    } catch (int exception) {
        std::cout << "Caught exception: " << exception << std::endl;
    }
}

int main() {
    std::cout << "Inside main" << std::endl;
    try {
        outerFunction();
    } catch (const char* exception) {
        std::cout << "Caught exception: " << exception << std::endl;
    }
    std::cout << "Exiting main" << std::endl;
    return 0;
}

In this example, we have four functions: main, outerFunction, middleFunction, and innerFunction. The innerFunction throws an exception of type const char* with a message.

When the program starts executing in the main function, it calls outerFunction, which in turn calls middleFunction. Inside middleFunction, the exception is thrown in innerFunction.

Since innerFunction does not handle the exception itself, the function terminates and is removed from the call stack. C++ then examines the call stack and identifies middleFunction as the top function that called the terminated innerFunction. As middleFunction has a try block, it checks the catch handlers for a match. However, as there is no catch handler for const char* in the try block, middleFunction is also removed from the call stack.

Finally, the program control reaches the catch block in main, which matches the thrown exception type const char*. The catch block is executed, and the appropriate message is displayed.

The output of the program would be:

Inside main
Inside outerFunction
Inside middleFunction
Inside innerFunction
Caught exception: Exception occurred in innerFunction
Exiting main

As shown in the example, stack unwinding occurs when a function throws an exception that is not handled within the function itself. The process continues until an appropriate catch handler is found or the program terminates.

User defined exception classes

It is recommended to create our own exception classes to make the type of the exception explicit and specific to our application. Best practice dictates that we throw objects instead of primitive types, and it is advisable to throw by value and catch by reference or const reference. There are additional best practices that we will cover in later discussions. Now, let’s explore how to create our own exception classes.

In this example, we will create two exception classes: DivideByZeroException and NegativeValueException. These classes will represent specific exceptions for our application. We can provide constructors, attributes, and methods for these classes as we would for any other class, but for simplicity, let’s keep them basic.

class DivideByZeroException {
};

class NegativeValueException {
};

With our custom exception classes defined, let’s modify the calculateMilesPerGallon function from the previous section to throw these exception objects:

double calculateMilesPerGallon(int miles, int gallons) {
    if (gallons == 0) {
        throw DivideByZeroException();
    }

    if (miles < 0 || gallons < 0) {
        throw NegativeValueException();
    }

    return static_cast<double>(miles) / gallons;
}

In this updated code, we check if gallons are zero and throw a DivideByZeroException object if it is. Additionally, if either miles or gallons is negative, we throw a NegativeValueException object.

Now, let’s see how we can handle these exceptions when calling the calculateMilesPerGallon function:

int main() {
    try {
        double result = calculateMilesPerGallon(300, 0);
        std::cout << "Miles per gallon: " << result << std::endl;
    } catch (const DivideByZeroException& exception) {
        std::cout << "Error: Division by zero." << std::endl;
    } catch (const NegativeValueException& exception) {
        std::cout << "Error: Negative value encountered." << std::endl;
    }

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

In the main function, we wrap the call to calculateMilesPerGallon inside a try block. If an exception is thrown during the execution of calculateMilesPerGallon, it will be caught by the appropriate catch block. In this case, we have catch blocks for both DivideByZeroException and NegativeValueException.

By catching the exceptions using const references, we have the option to access any attributes or methods of the exception objects if we had defined them in our custom exception classes.

If none of the catch blocks match the thrown exception type, the program will terminate or continue searching for a suitable handler up the call stack.

In summary, by creating our own exception classes and throwing them, we can provide more specific and meaningful exceptions for our application. By catching these exceptions, we can handle them appropriately and continue the execution of our program.

Populating user defined exception classes for added features

When implementing a user-defined exception class in C++, you have the flexibility to include various features to enhance the functionality and information provided by the exception. Here are some things you can implement in a user-defined exception class:

Custom error message

You can provide a descriptive error message that explains the nature of the exception. This can be done by overriding the what() function inherited from the base std::exception class. Here’s the syntax to do that:

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

Output:

Exception caught: Custom error message.

Additional attributes

You can add additional attributes to the exception class to provide more specific information about the error. These attributes can be accessed in the catch block to handle the exception accordingly. Here’s the syntax to do that:

class MyException : public std::exception {
private:
    int errorCode;
public:
    MyException(int code) : errorCode(code) {}
    
    int getErrorCode() const {
        return errorCode;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>

class MyException : public std::exception {
private:
    int errorCode;
public:
    MyException(int code) : errorCode(code) {}
    
    int getErrorCode() const {
        return errorCode;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

int main() {
    try {
        throw MyException(42);
    } catch (const MyException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
        std::cout << "Error code: " << e.getErrorCode() << std::endl;
    }

    return 0;
}

Output:

Exception caught: Custom error message.
Error code: 42

Contextual information

You can include additional data or context information that helps in understanding the cause of the exception. This can be achieved by defining member variables and providing appropriate constructors to initialise them. Here’s the syntax to do that:

class MyException : public std::exception {
private:
    std::string context;
public:
    MyException(const std::string& ctx) : context(ctx) {}
    
    const std::string& getContext() const {
        return context;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

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

class MyException : public std::exception {
private:
    std::string context;
public:
    MyException(const std::string& ctx) : context(ctx) {}
    
    const std::string& getContext() const {
        return context;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

int main() {
    try {
        throw MyException("An error occurred in the function foo()");
    } catch (const MyException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
        std::cout << "Context: " << e.getContext() << std::endl;
    }

    return 0;
}

Output:

Exception caught: Custom error message.
Context: An error occurred in the function foo()

Nested exceptions

You can include a nested exception within your user-defined exception class. This allows you to capture and propagate multiple exceptions, providing more comprehensive error handling. Here’s the syntax to do that:

class MyException : public std::exception {
private:
    std::exception_ptr nestedException;
public:
    MyException(std::exception_ptr nested) : nestedException(nested) {}
    
    std::exception_ptr getNestedException() const {
        return nestedException;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

Example

#include <iostream>
#include <exception>
#include <stdexcept>

class MyException : public std::exception {
private:
    std::exception_ptr nestedException;
public:
    MyException(std::exception_ptr nested) : nestedException(nested) {}
    
    std::exception_ptr getNestedException() const {
        return nestedException;
    }
    
    const char* what() const noexcept override {
        return "Custom error message.";
    }
};

void innerFunction() {
    throw std::runtime_error("Inner exception");
}

void outerFunction() {
    try {
        innerFunction();
    } catch (const std::exception& e) {
        std::exception_ptr nested = std::current_exception();
        throw MyException(nested);
    }
}

int main() {
    try {
        outerFunction();
    } catch (const MyException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
        std::exception_ptr nested = e.getNestedException();
        try {
            std::rethrow_exception(nested);
        } catch (const std::exception& nestedException) {
            std::cout << "Nested exception: " << nestedException.what() << std::endl;
        }
    }

    return 0;
}

Output

Exception caught: Custom error message.
Nested exception: Inner exception

As you can see, it is possible to throw an exception from within an exception handler. This allows for the chaining or nesting of exceptions, where an outer exception captures and includes an inner exception. The above example is bit complex and here is the breakdown of the code:

  1. We define a custom exception class called MyException, derived from std::exception. This class has an additional member variable nestedException of type std::exception_ptr, which will hold the nested exception.
    1. std::exception is a base class in C++ for defining user-defined exception classes. It provides a standard interface for handling exceptions and serves as the parent class for all standard exceptions in C++.
    2. std::exception_ptr is a C++ type that represents a pointer to an exception object. It allows capturing and storing an exception for later rethrowing or inspection.
  2. The innerFunction throws a std::runtime_error exception.
  3. The outerFunction calls innerFunction within a try-catch block. If an exception is thrown, it captures the exception using std::current_exception() to obtain an std::exception_ptr to the nested exception.
    1. std::current_exception() is a C++ function that captures the currently thrown exception and returns an std::exception_ptr pointing to it.
  4. The MyException constructor takes the nested exception as a parameter and stores it in the nestedException member variable.
  5. In the main function, we call outerFunction within a try-catch block. If a MyException is thrown, we catch it and retrieve the nested exception using getNestedException().
  6. We then rethrow the nested exception using std::rethrow_exception and catch it again. In this case, the nested exception is a std::runtime_error that was thrown in the innerFunction.
    1. std::rethrow_exception is a C++ function that rethrows an exception represented by an std::exception_ptr.
    2. std::runtime_error is a C++ standard exception class that represents errors that can occur during runtime.

By using nested exceptions, we can capture more detailed information about the error and propagate it up the call stack. It allows us to handle exceptions at different levels of the program and gain insights into the origin and nature of the error.

In the provided code, the MyException class acts as a wrapper for the nested exception, providing a custom error message. By using the std::exception_ptr type, we can capture and propagate exceptions of any type, making it a flexible mechanism for error handling and reporting.

These are just a few examples of what you can implement in a user-defined exception class. The specific features and attributes depend on the requirements of the application and the information you want to convey when an exception occurs. Remember to consider best practices, such as throwing objects instead of primitive types and catching exceptions by reference or const reference to ensure proper exception handling and avoid unnecessary object slicing.

Exceptions in the context of c++ class

Exceptions can indeed be thrown from class methods, constructors, and destructors. However, it is important to follow best practices for each scenario.

Destructors should not throw exceptions. By default, destructors are marked as noexcept in C++, which means they are not expected to throw exceptions. If a destructor throws an exception while being called as a result of another exception, the original catch block will not be reached, leading to a problematic situation. It is highly discouraged to throw exceptions from destructors, unless the destructor can handle and resolve the exception internally, although such cases are rare.

For class methods, exception handling works similarly to regular functions. Exceptions can be thrown, caught, and handled appropriately using try-catch blocks.

Constructors, on the other hand, can also throw exceptions. Constructors may fail for various reasons, such as failing to allocate memory dynamically or encountering errors during initialization. Since constructors do not have return values, exceptions provide a mechanism to handle these failure scenarios. For example, in a class like Account, if the constructor detects a negative balance, it can throw a custom exception like IllegalBalanceException.

Here’s an example that demonstrates throwing an exception from a constructor:

#include <iostream>
#include <memory>

class IllegalBalanceException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Illegal balance detected";
    }
};

class Account {
private:
    double balance;

public:
    Account(double initialBalance) : balance(initialBalance) {
        if (balance < 0)
            throw IllegalBalanceException();
    }
};

int main() {
    try {
        Account moeAccount (-1000.0);
    }
    catch (const IllegalBalanceException& ex) {
        std::cout << "Exception caught: " << ex.what() << std::endl;
    }

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

    return 0;
}

Output:

Exception caught: Illegal balance detected
Program continues after exception handling.

In this example, the Account class’s constructor takes an initial balance as a parameter. If the provided balance is negative, the constructor throws an IllegalBalanceException. In the main function, we use a try-catch block to catch this exception. The caught exception is then displayed, and the program continues execution.

By handling exceptions in constructors, we can gracefully handle construction failures and provide appropriate error handling or feedback to the caller.

C++ standard exception class hierarchy

In C++, the standard exception class hierarchy is defined in the <stdexcept> header and provides a set of predefined exception classes that represent common error conditions. These exception classes are derived from the base class std::exception. The following diagram shows the C++ standard exception class hierarchy:

Some of the important and most frequently used classes are as follows:

  1. std::exception: This is the base class for all standard exceptions. It defines the basic interface for exceptions, including a virtual member function what() that returns a C-style string describing the exception.
  2. std::logic_error: This class is used to represent errors that are caused by logical errors in the program. It has several derived classes:
  3. std::invalid_argument: This exception is thrown when an invalid argument is passed to a function or constructor.
  4. std::domain_error: This exception is thrown when a value is outside the valid domain for a mathematical function or operation.
  5. std::length_error: This exception is thrown when a length or size exceeds the maximum allowed limit.
  6. std::out_of_range: This exception is thrown when an index or value is out of the valid range.
  7. std::runtime_error: This class is used to represent errors that occur during runtime. It has several derived classes:
  8. std::range_error: This exception is thrown when an attempt is made to store a value outside the range of the data type.
  9. std::overflow_error: This exception is thrown when an arithmetic operation results in an overflow.
  10. std::underflow_error: This exception is thrown when an arithmetic operation results in an underflow.
  11. std::system_error: This exception is used to represent errors related to the operating system or external libraries.
  12. std::bad_alloc: This exception is thrown when a dynamic memory allocation fails, typically due to insufficient memory.

These standard exception classes provide a convenient way to handle common error scenarios in a consistent and standardised manner. We can create subclasses of the exception classes and then implement the what virtual function so that it displays whatever exception message we want. This can be very useful in some situations since by being derived publicly from std::exception, our classes are now part of this hierarchy and can be used wherever a std::exception is expected since your class is an exception.

Also thanks to dynamic polymorphism. All you have to do is implement the what() virtual function and it will be bound dynamically at runtime.

By catching specific exception types, you can handle different types of errors appropriately in the code.

Here’s an example demonstrating the usage of some standard exception classes:

#include <iostream>
#include <stdexcept>

int main() {
    try {
        // Throwing an exception of type std::invalid_argument
        throw std::invalid_argument("Invalid argument!");
    } catch (const std::exception& ex) {
        std::cout << "Caught exception: " << ex.what() << std::endl;
    }
    return 0;
}

Output

Caught exception: Invalid argument!

In this example, an exception of type std::invalid_argument is thrown and caught. The what() member function is used to retrieve the error message associated with the exception, which is then printed to the console.

User-defined exception by inheriting from a standard exception class

Let’s explore an example that demonstrates the creation of a user-defined exception by inheriting from a standard exception class.

#include <iostream>
#include <exception>
#include <memory>

class IllegalBalanceException : public std::exception {
    public:
        IllegalBalanceException() = default;
        ~IllegalBalanceException() noexcept = default;
        const char* what() const noexcept override {
            return "Illegal balance exception";
        }
};

class Account {
    private:
        double balance;
    public:
        Account(double initialBalance) {
            if (initialBalance < 0)
                throw IllegalBalanceException();
            balance = initialBalance;
        }
        double getBalance() const {
            return balance;
        }
};

int main()
{
    try {
        std::unique_ptr<Account> moeAccount;
        moeAccount = std::make_unique<Account>(-100);
        std::cout << "Moe's account balance: " << moeAccount->getBalance() << std::endl;
    }
    catch (const std::exception& ex) {
        std::cout << "Exception caught: " << ex.what() << std::endl;
    }
    return 0;
}

Output:

Exception caught: Illegal balance exception

In this example, we define a user-defined exception class called IllegalBalanceException, which publicly inherits from std::exception. The class provides a default constructor and a default destructor. We also override the what() virtual function to return a C-style string description of the exception.

Next, we have the Account class that uses the IllegalBalanceException to handle the case when an account is created with a negative balance. In the constructor, if the initial balance is less than 0, we throw an IllegalBalanceException object.

In the main() function, we create a std::unique_ptr to an Account object, attempting to create Moe’s account with a negative balance of -100. Since this is an illegal balance, the Account constructor throws an IllegalBalanceException. We catch the exception in the catch block, where we can access the exception object and call its what() function to display the exception message.

Exercises

  1. Write a program that prompts the user to enter two integers and performs division. Handle the division by zero exception and display an error message if the second number is zero.
  2. Create a class called “TemperatureConverter” that converts temperatures between Celsius and Fahrenheit. Add a member function named “convertToFahrenheit” that takes a temperature in Celsius as input and converts it to Fahrenheit. Handle any invalid temperature values (e.g., below absolute zero) by throwing a custom exception. Write a program that utilises the class and catches the exception to display an error message.
  3. Write a program that reads a file name from the user and attempts to open and read the contents of the file. Handle any file-related exceptions, such as file not found or permissions error, and display an appropriate error message.
  4. Implement a stack class that supports push, pop, and top operations. Handle the exception when popping an element from an empty stack and display an error message.
  5. Write a program that asks the user to enter a positive integer and calculates its factorial. Handle the exception when a negative integer is entered and display an error message.
  6. Write a program that prompts the user to enter a positive number. If the user enters a negative number, throw an instance of std::domain_error with an appropriate error message.
  7. Create a function that takes two integers as input and divides them. Handle the case where the second number is zero by throwing an instance of std::runtime_error with a descriptive error message.
  8. Write a program that reads a file name from the user and attempts to open and read the contents of the file. Handle the case where the file does not exist by throwing an instance of std::invalid_argument with an appropriate error message.
  9. Implement a vector class that supports indexing. Handle the case where an index is out of bounds by throwing an instance of std::out_of_range with a descriptive error message.
  10. Write a program that asks the user to enter their age. If the user enters a value less than 0 or greater than 120, throw an instance of std::range_error with a suitable error message.
  11. Create a user-defined exception class named NegativeValueException that is derived from std::logic_error. This exception should be thrown when a negative value is encountered. Write a program that asks the user to enter a positive number, and if they enter a negative number, throw an instance of NegativeValueException with an appropriate error message.
  12. Implement a class named Circle that represents a circle. Include a constructor that takes a radius as a parameter. Throw an instance of std::invalid_argument with an appropriate error message if a negative radius is provided.
  13. Create a user-defined exception class named FileReadException that is derived from std::runtime_error. This exception should be thrown when a file cannot be read. Write a program that attempts to read the contents of a file specified by the user, and if the file cannot be read, throw an instance of FileReadException with a descriptive error message.
  14. Implement a class named Student that represents a student. Include a constructor that takes a student’s age as a parameter. Throw an instance of std::range_error with an appropriate error message if an age outside the valid range (18-25) is provided.
  15. Create a user-defined exception class named OverflowException that is derived from std::runtime_error. This exception should be thrown when an arithmetic operation results in an overflow. Write a program that performs arithmetic operations on large numbers and throws an instance of OverflowException if an overflow occurs.
  16. Write a program that prompts the user to enter two integers and performs division operation. Handle the following exceptions:
    1. If the user enters non-numeric input, throw a std::invalid_argument exception with an appropriate error message.
    2. If the user enters a divisor of 0, throw a std::runtime_error exception with an appropriate error message.
    3. If the result of the division is too large to fit in an int variable, throw a std::overflow_error exception with an appropriate error message.
  17. Implement a function called calculateSquareRoot that takes an integer as input and calculates its square root. Handle the following exceptions:
    1. If the input is negative, throw a std::domain_error exception with an appropriate error message.
    2. If the input is 0, throw a std::runtime_error exception with an appropriate error message.
    3. If the input is too large to fit in an int variable, throw a std::overflow_error exception with an appropriate error message.
  18. Create a class called FileReader that reads the contents of a file. Implement a member function called readFile that takes a file path as input and handles the following exceptions:
    1. If the file does not exist, throw a std::runtime_error exception with an appropriate error message.
    2. If the file cannot be opened for reading, throw a std::ios_base::failure exception with an appropriate error message.
  19. Write a program that asks the user to enter a character. Handle the following exceptions:
    1. If the user enters more than one character, throw a std::length_error exception with an appropriate error message.
    2. If the user enters a non-alphabetic character, throw a std::invalid_argument exception with an appropriate error message.
  20. Implement a function called divideNumbers that takes two integers as input and performs division operation. Handle the following exceptions:
    1. If the first number is negative, throw a std::range_error exception with an appropriate error message.
    2. If the second number is zero, throw a std::logic_error exception with an appropriate error message.
    3. If the division result is not an integer, throw a std::runtime_error exception with an appropriate error message.

Interview questions

  1. What is exception handling in C++? How does it work?
  2. What are the benefits of using exceptions in a program?
  3. Explain the difference between the try block, catch block, and throw statement in C++.
  4. How do you handle exceptions in C++? Explain the catch hierarchy and how multiple catch blocks are evaluated.
  5. What is the purpose of the std::exception class in C++? How is it used in exception handling?
  6. Can you give an example of how to create a user-defined exception in C++?
  7. Explain the concept of stack unwinding in exception handling.
  8. What are the best practices for exception handling in C++?
  9. Can you throw an exception from a destructor? Why or why not?
  10. How do you handle exceptions in constructors? Are there any special considerations?
  11. What are the differences between noexcept, throw(), and noexcept() in C++?
  12. How do you handle exceptions that are not caught by any catch block?
  13. What are the standard exception classes provided by C++? Can you give examples of when you might use them?
  14. Explain the concept of object slicing in the context of exception handling.
  15. Can you explain how to use the try-catch construct in C++?

Leave a Reply

Your email address will not be published. Required fields are marked *

You cannot copy content of this page