Exceptions in C++

In C++, exceptions are a mechanism for handling runtime errors or exceptional situations that occur during the execution of a program. Exceptions provide a way to gracefully and safely handle errors without abruptly terminating the program. Instead of letting an error crash the program, you can catch and handle it, allowing the program to continue running or performing some cleanup operations.

Here's an overview of how exceptions work in C++:

  1. Throwing an Exception: When an exceptional situation occurs in your code, you can use the throw keyword to throw an exception. An exception is typically an object of a class derived from the std::exception class or a class that inherits from it. For example:

     throw std::runtime_error("An error occurred.");
    
  2. Catching an Exception: To catch and handle exceptions, you use a try...catch block. Within the try block, you place the code that might throw an exception. In the catch block, you specify the type of exception you want to catch and define the handling code. For example:

     try {
         // Code that might throw an exception
     } catch (const std::exception& e) {
         // Handle the exception
         std::cerr << "Exception caught: " << e.what() << std::endl;
     }
    

    In this example, the catch block catches any exception derived from std::exception and prints an error message.

  3. Multiple Catch Blocks: You can have multiple catch blocks to handle different types of exceptions. The first catch block whose exception type matches the thrown exception will be executed.

     try {
         // Code that might throw an exception
     } catch (const std::runtime_error& e) {
         // Handle runtime_error
     } catch (const std::logic_error& e) {
         // Handle logic_error
     } catch (const std::exception& e) {
         // Handle other exceptions derived from std::exception
     } catch (...) {
         // Handle all other exceptions
     }
    
  4. Rethrowing Exceptions: You can rethrow an exception within a catch block using throw;. This allows you to catch an exception, perform some additional handling, and then propagate the exception to an outer try...catch block.

     try {
         // Code that might throw an exception
     } catch (const std::exception& e) {
         // Handle the exception
         throw; // Rethrow the same exception
     }
    
  5. Custom Exceptions: You can define your own exception classes by deriving them from std::exception or another appropriate base class. This allows you to create exception hierarchies tailored to your application's needs.

     class MyCustomException : public std::exception {
     public:
         MyCustomException(const std::string& message) : msg(message) {}
    
         const char* what() const noexcept override {
             return msg.c_str();
         }
    
     private:
         std::string msg;
     };
    
  6. Exception Safety: C++ provides support for exception safety, which is the practice of designing code to handle exceptions gracefully and avoid resource leaks or corruption.

Exceptions are a powerful tool for managing error conditions in C++, but they should be used judiciously, and performance considerations should be taken into account when using them in critical sections of code. Additionally, it's important to document and communicate the exceptions that functions and methods can throw so that users of your code can handle them appropriately.

Types of throws available:

C++ provides a standard library of exceptions that you can use to handle various error conditions in your code. These exceptions are part of the <stdexcept> header and are derived from the std::exception class. Here are some commonly used standard exceptions in C++:

  1. std::exception: This is the base class for all standard C++ exceptions. It provides a what() member function that returns a description of the exception.

  2. std::runtime_error: This exception is used to indicate errors that can only be determined at runtime, such as attempting to open a file that doesn't exist or performing an invalid operation on a data structure.

  3. std::logic_error: This exception is used to indicate errors that can be detected at compile-time, such as dividing by zero or accessing an out-of-bounds array element.

  4. std::invalid_argument: Thrown when a function receives an argument of the correct data type but with an invalid value. For example, passing a negative value to a function that expects a positive integer.

  5. std::out_of_range: Thrown when an attempt is made to access an element outside the valid range of a data structure, such as an array or a container.

  6. std::overflow_error: This exception is thrown when an arithmetic operation overflows. For example, if the result of an integer division exceeds the maximum value representable by the data type.

  7. std::underflow_error: Thrown when an arithmetic operation underflows, meaning the result is too small to be represented by the data type.

  8. std::bad_alloc: This exception is thrown when the new operator fails to allocate memory, typically due to insufficient available memory.

  9. std::bad_cast: Thrown when a dynamic cast (dynamic_cast) fails because the object being casted does not match the target type.

  10. std::bad_typeid: Thrown when the typeid operator is used on a null pointer or a reference to a null pointer.

  11. std::length_error: Thrown when a function, like std::vector::resize(), is called with an argument that would result in a size too large for the container to handle.

  12. std::range_error: Thrown when a value is outside the valid range for a specific operation. For example, using std::stoi to convert a string that represents a value beyond the range of the target type.

  13. std::domain_error: Used to signal that an argument value is outside the expected domain of a mathematical function, like the square root of a negative number.

These are some of the commonly used standard exceptions in C++. You can also create custom exceptions by inheriting from std::exception or its derived classes to represent specific error conditions in your application. When using exceptions, it's important to catch and handle them appropriately to provide robust error handling in your code.

Example 1:

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("Some error occurred.");
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Unknown exception caught." << std::endl;
    }

    return 0;
}

Example 2: Unknown Exception

#include <iostream>
#include <stdexcept>

int main() {
    try {
        // Attempt to perform some operations that may throw exceptions
        int x = 5;
        int y = 0;

        // Division by zero (will throw an exception)
        int result = x / y;

        // Attempt to access an out-of-bounds array element (will throw an exception)
        int arr[3] = {1, 2, 3};
        int value = arr[5];
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Unknown exception caught." << std::endl;
    }

    return 0;
}

Example 3:

#include <iostream>
#include <typeinfo>
#include <stdexcept>

class Base {
public:
    virtual void print() const {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() const override {
        std::cout << "Derived class" << std::endl;
    }
};
class Derived1 : public Base {
public:
    void print() const override {
        std::cout << "Derived class1" << std::endl;
    }
};

int main() {


   try {
        Base baseObj;
        Derived d;
        Base& baseRef = d;
        // Performing Crazy cast
        Derived1& err = dynamic_cast<Derived1&>(baseRef);
        err.print();
    } catch (const std::bad_cast& e) {
        std::cerr << "Bad cast caught: " << e.what() << std::endl;
    }
    return 0;
}