Exception handling is an important aspect of C++ programming that allows for graceful handling of errors and unexpected events in your code. In this tutorial, we will explore some advanced techniques for handling exceptions in C++.

Table of Contents

Exception handling in C++

One of the major features in C++ is exception handling, which is a better way of thinking about and handling errors. With exception handling the following statements apply:

  1. Writing error-handling code is less tedious and avoids mixing it with your main code. You can write your desired code first, and then handle potential errors separately. If you make multiple calls to a function, you can handle any errors from that function in one place.
  2. Errors must be addressed and cannot be ignored. When a function encounters an error, it throws an object representing the error to the caller. If the caller doesn’t catch the error and handle it, the error moves to the next level of dynamic scope until it’s caught or the program terminates because there is no handler for that type of exception.

This article first examines C Programming Approach to error handling and discusses why it does not work well for C, and explains why it does not work C++ too. This article also covers try, throw, and catch, the C++ keywords that support exception handling.

Error handling in C

Error handling can be simple when you have all the information you need to handle the error in the current context. However, if you lack information, you’ll need to pass the error information to a different context where that information is available. In C, you can address this issue with three approaches:

  1. Return error information from the function: To handle errors, you can return error information from a function or set a global error condition flag. However, this can lead to the programmer ignoring error information due to tedious error checking with each function call.
  2. Use the little-known Standard C library signal-handling system: Another approach is to use the signal-handling system in Standard C with the signal() function and raise() to generate an event. But this approach requires understanding and installing the appropriate signal-handling mechanism, which can be challenging in large projects.
  3. Use the nonlocal go to functions in the Standard C library: The nonlocal go-to functions in Standard C, setjmp() and longjmp(), can also be used. With setjmp(), you save a known good state, and longjmp() restores that state if you encounter an error. However, there is high coupling between the place where the state is stored and

When dealing with error-handling in C++, there’s a critical issue with the use of signals and setjmp()/longjmp(). These methods do not call destructors, meaning that objects are not properly cleaned up. This makes it challenging to recover from exceptional conditions, as objects will be left behind that cannot be accessed or cleaned up. For example, using setjmp()/longjmp() can result in undefined behavior if it jumps past the end of a scope where destructors should be called.

Throwing an exception

If you encounter an exceptional situation in your code? That is, one in which you don’t have enough information in the current context to decide what to do you can send information about the error into a larger context by creating an object that contains that information and ?throwing? it out of your current context. This is called throwing an exception. Here’s what it looks like:

To throw exceptions in C++, you can use any type, including built-in types, but it’s common to create special classes for exceptions. MyError is one such class that takes a char* as a constructor argument.

When you throw an object in C++, the throw keyword creates a copy of the object and essentially returns it from the function, even if that object type isn’t what the function normally returns. However, throwing an exception is not the same as a regular function return because it takes you to a different part of the code called an exception handler. This handler might be far away from where the exception was thrown, and any local objects created before the exception occurred are automatically cleaned up through a process called “stack unwinding.” It is important because it ensures that all objects on the stack are properly cleaned up when an exception is thrown, even if the exception is not caught.

Stack unwinding in C++ refers to the process of deallocating and cleaning up the objects on the stack when an exception is thrown. When an exception is thrown, the C++ runtime searches for the nearest exception handler that can handle the exception.

Catching an exception

As mentioned earlier, one of the advantages of C++ exception handling is that it allows you to concentrate on the problem you’re actually trying to solve in one place, and then deal with the errors from that code in another place.

It is important to understand the basic structure of an exception handling block. This block consists of a try block, which contains the code that might throw an exception, and one or more catch blocks, which handle the exception if it is thrown.

The try block

The try block is an ordinary scope, preceded by the keyword try:

Exception handlers

The thrown exception must end up some place. This place is the exception handler, and you need one exception handler for every exception type you want to catch. Exception handlers immediately follow the try block and are denoted by the keyword catch:

The syntax of a catch clause resembles functions that take a single argument. The identifier (id1, id2, and so on) can be used inside the handler, just like a function argument, although you can omit the identifier if its not needed in the handler. The exception type usually gives you enough information to deal with it.

To illustrate using try and catch, the following variation of C++ program replaces the call to setjmp() with a try block and replaces the call to longjmp() with a throw statement.

When the throw statement is executed, the program goes back until it finds the catch clause that takes a matching parameter, where execution resumes. In the program example above, the destructor for the object “rb” is called when the throw statement causes execution to leave the function “oz()”.

There are two models for exception handling: termination and resumption. In the termination model (supported by C++), the error is considered too severe to automatically resume execution at the point where the exception occurred. In contrast, the resumption model (introduced with the PL/I language) assumes that the exception handler can fix the problem, and the code is retried automatically. If you want to use resumption in C++, you need to explicitly transfer execution back to the code where the error occurred, often through a while loop. However, in practice, resumption is not used as much, perhaps because of the conceptual difficulty in large systems with many potential points of exception.

Exception Matching

When an exception is thrown, the system searches for the nearest handlers in the order they appear in the code. When it finds a matching handler, the exception is considered handled and the search stops. Matching an exception with a handler doesn’t require an exact match; an object or reference to a derived class object can match a handler for the base class.

However, if the handler is for an object rather than a reference, the exception object is truncated to the base type when passed to the handler. Therefore, it is always better to catch an exception by reference instead of by value. If a pointer is thrown, the standard pointer conversions are used to match the exception. Automatic type conversions are not used to convert from one exception type to another in the process of matching.

The following example shows how a base-class handler can catch a derived-class exception:

When an exception is thrown, the first matching handler is used to handle it. In this example, the first handler will catch any object or anything derived from it, so the second and third handlers will never be used. It’s better to catch more specific derived types first, and the base type last to catch anything less specific. It’s recommended to use reference arguments instead of value arguments in exception handlers to avoid losing information.

Re-throwing an Exception

You usually want to re-throw an exception when you have some resource that needs to be released, such as a network connection or heap memory that needs to be deallocated. After that, you’ll want to let some other context closer to the user handle the exception.

You want to catch any exception, clean up your resource, and then re-throw the exception so that it can be handled elsewhere. You re-throw an exception by using throw with no argument inside a handler:

Uncaught exceptions

Exception handling is preferable to returning error codes because exceptions can’t be ignored. If an exception is not handled by any of the exception handlers following a particular try block, it moves up to the next higher context, which is either the function or the try block surrounding the one that did not catch the exception. The location of this try block is not always evident at first sight, as it is higher up in the call chain. This procedure continues until a handler at some level matches the exception, at which point the exception is considered “caught,” and no further search is performed.

The terminate() function

If an exception is not caught by any handler, the function terminate() from the <exception> header is automatically called. By default, terminate() calls the abort() function from the Standard C library, which abruptly exits the program and does not execute any destructors for global or static objects. On Unix systems, abort() also causes a core dump. The terminate() function is also executed if a destructor for a local object throws an exception during stack unwinding or if a global or static object’s constructor or destructor throws an exception. In general, it is not recommended to allow a destructor to throw an exception.

The set_terminate() function

The set_terminate() function allows you to set a custom terminate() function to handle exceptions that are not caught by any handlers. The function returns a pointer to the previous terminate() function so that you can restore it later. Your custom terminate() function must take no arguments and return void. It must also not throw an exception or return, but instead execute program-termination logic. If terminate() is called, it means that the problem is unrecoverable.

Cleaning up

When an exception is thrown and caught, the program jumps from its normal flow to the corresponding exception handler. To make this feature useful, C++ guarantees that as you leave a scope, all objects in that scope whose constructors have been completed will have their destructors called. This ensures that things are cleaned up properly when an exception is thrown.

Here’s an example C++ program shows what happens when an exception is thrown in the middle of the creation of an array of objects:

You can see the results in the output of the program:

Resource Management

When using exceptions in your code, it’s crucial to consider whether your resources will be cleaned up correctly if an exception occurs. Constructors are particularly problematic because if an exception is thrown before the constructor completes, the associated destructor won’t be called for that object. This is especially challenging when allocating resources in constructors, especially when dealing with “naked” pointers. If an exception occurs in the constructor, the destructor won’t have a chance to deallocate the resource, which can lead to memory leaks.

The output of the above C++ program is the following:
UseResources()
Cat()
Cat()
Cat()
allocating a Dog
inside handler

The UseResources constructor is entered, and the Cat constructor is successfully completed for the three array objects. However, inside Dog::operator new(), an exception is thrown (to simulate an out-of-memory error). Suddenly, you end up inside the handler, without the UseResources destructor being called. This is correct because the UseResources constructor was unable to finish, but it also means the Cat objects that were successfully created on the heap were never destroyed.

Making Everything an Object

There are two ways to prevent resource leaks when using raw resource allocations:

  1. Catch exceptions in the constructor and release the resource.
  2. Place the allocations inside an object’s constructor and the deallocations inside its destructor.

This approach ensures that all resource allocations become atomic and are part of the lifetime of a local object. If an allocation fails, the other resource allocation objects are properly cleaned up during stack unwinding. This technique is known as Resource Acquisition Is Initialization (RAII), which links resource control with object lifetime. Using templates is an excellent way to apply RAII.

The difference is the use of the template to wrap the pointers and make them into objects. The constructors for these objects are called before the body of the UseResources constructor, and any of these constructors that complete before an exception is thrown will have their associated destructors called during stack unwinding.

Now the output of the program is:

The auto_ptr class template

In C++, dynamic memory is commonly used, and the auto_ptr class template in the <memory> header is provided to manage it. auto_ptr takes a pointer to a generic type as a constructor argument, and overloads the * and -> pointer operators to act like the original pointer. So you can use the auto_ptr object just like a regular pointer. The benefit of auto_ptr is that it automatically frees the memory when it goes out of scope, preventing resource leaks.

The auto_ptr class template is also handy for pointer data members. Since class objects contained by value are always destructed, auto_ptr members always delete the raw pointer they wrap when the containing object is destructed.

Even though we didn’t explicitly delete the original pointer, pMyObject’s destructor deletes the original pointer during stack unwinding, as the following output verifies:

Allocating TraceHeap object on the heap at address 8930040
5
Deleting TraceHeap object at address 8930040

The auto_ptr was removed in C++17 because it had some serious drawbacks and limitations. One of the main issues was that auto_ptr did not provide a safe mechanism for transferring ownership of a pointer between two objects, leading to potential memory leaks or undefined behavior. Therefore, we recommend to use unique_ptr or shared_ptr instead of auto_ptr in modern C++ programs.

Function-level try blocks

Function-level try blocks are used to catch exceptions thrown from a specific function. They allow you to catch exceptions thrown by the code within the function without having to modify the function’s implementation.

Here’s an example code snippet to demonstrate function-level try blocks:

In this example, the divide function takes two double parameters and returns the result of dividing the first by the second. If the denominator is zero, the function throws an exception with a message “Division by zero!”.

The try block in the divide function catches the exception and outputs an error message to the standard error stream. It then re-throws the exception using the throw statement.

In the main function, a try block is used to call the divide function with a denominator of zero. This throws an exception, which is caught by the catch block in the main function. The catch-all catch block uses the ... ellipsis to catch any type of exception. It simply outputs a message to the standard error stream.

Function-level try blocks can be useful when you want to catch exceptions thrown from a specific function without having to modify the function’s implementation.

Standard Exceptions in Standard C++ library

The Standard C++ library provides a set of standard exception classes that can be thrown and caught by programs. These exception classes are defined in the <stdexcept> header file and are organized into a hierarchy, with the base exception class being std::exception. The standard exception classes include:

  1. std::logic_error: This class is used to indicate errors that are related to the logic of the program. It has two derived classes: std::invalid_argument and std::domain_error.
  2. std::runtime_error: This class is used to indicate errors that occur during the execution of the program. It has several derived classes, including std::range_error, std::overflow_error, and std::underflow_error.
  3. std::bad_alloc: This class is used to indicate errors that occur when the program is unable to allocate memory.
  4. std::bad_cast: This class is used to indicate errors that occur when a dynamic_cast operation fails.
  5. std::bad_typeid: This class is used to indicate errors that occur when a typeid operation is applied to an object of a polymorphic class that has been destroyed.
  6. std::bad_exception signifies an incorrect exception was thrown.
  7. std::bad_function_call thrown by “null” std::function
  8. std::bad_weak_ptr constructing a shared_ptr from a bad weak_ptr

Here’s an example that demonstrates how to throw and catch a standard exception:

In this example, the function myFunction() checks the value of its parameter x and throws an exception of type std::invalid_argument if x is negative. The main() function calls myFunction() with a negative argument, which results in an exception being thrown. The catch block catches the exception and prints an error message to the standard error stream.

Exception specifications

It’s not required to tell people what exceptions your function might throw, but it’s a good practice because users won’t know how to catch all potential exceptions without this information. If they have your source code, they can look for throw statements, but often a library doesn’t come with sources. Good documentation can help, but it’s not always available. C++ has a syntax to specify the exceptions a function can throw, so users can handle them. This is called an optional exception specification, which goes after the function’s arguments.

The exception specification reuses the keyword throw, followed by a parenthesized list of all the types of potential exceptions that the function can throw. Your function declaration might look like this:

In this example, the function myFunction may throw MyException1 or MyException2.

It’s important to note that exception specifications are not enforced by the C++ compilers, so a function may throw exceptions that are not listed in its exception specification. Additionally, using exception specifications can make your code more difficult to maintain, since adding or removing an exception from a function’s implementation may require updating the exception specification as well.

As a result, exception specifications are generally not recommended in modern C++ and exception specifications are deprecated as of C++17. Instead, it is more common to use noexcept and std::exception to convey information about a function’s exception handling behavior.

Uses of Exceptions in C++

Exceptions in C++ are used in a variety of situations to handle errors and unexpected events. Here are some typical uses of exceptions in C++:

  1. Error handling: Exceptions are often used to handle errors and exceptions that occur during runtime. For example, if a file cannot be opened or read, an exception can be thrown to handle the error.
  2. Resource management: Exceptions can be used to manage resources such as memory, file handles, and network connections. If a resource cannot be allocated or released, an exception can be thrown to handle the error.
  3. Program flow control: Exceptions can be used to control the flow of a program. For example, if an error occurs, the program can throw an exception to jump to a catch block that will handle the error and allow the program to continue executing.
  4. User input validation: Exceptions can be used to validate user input and handle invalid input. For example, if a user enters an invalid value, an exception can be thrown to handle the error.
  5. Assertion failures: Exceptions can be used to handle assertion failures, which occur when a program encounters a condition that should not happen. An exception can be thrown to handle the error and provide useful debugging information.
  6. Wrap Functions: Wrap functions (especially C library functions) that use ordinary error schemes so they produce exceptions instead.
  7. Program Safety: By using exceptions in your library and program can increase their safety, which can benefit you in both the short-term for debugging and the long-term for ensuring application robustness.

These are just a few typical uses of exceptions in C++, and there are many other scenarios where exceptions can be useful for error handling and control flow.

When to use exception specifications?

The use of exception specifications in C++ programs is a controversial topic. Exception specifications were introduced to help programmers document the exceptions that a function might throw and to allow the compiler to enforce these requirements at compile-time. However, the use of exception specifications can also limit the flexibility of a function and can lead to unexpected behavior.

As a general guideline, it is recommended to avoid using exception specifications unless there is a compelling reason to use them. Some cases where exception specifications might be appropriate include:

  1. When writing a library function that must be compatible with an existing interface that uses exception specifications.
  2. When writing a low-level system function that needs to guarantee that it will not throw exceptions in certain situations.
  3. When writing a function that is part of a safety-critical system and where the cost of failure is very high.

In most other cases, it is better to use good documentation and defensive programming techniques to ensure that exceptions are handled properly, rather than relying on exception specifications.