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++
- Error handling in C
- Throwing an exception
- Catching an exception
- Exception Matching
- Re-throwing an Exception
- Uncaught exceptions
- Cleaning up
- Resource Management
- Function-level try blocks
- Standard Exceptions in Standard C++ library
- Exception specifications
- Uses of Exceptions in C++
- When to use exception specifications?
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:
- 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.
- 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:
- 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.
- 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.
- 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <iostream> #include <csetjmp> using namespace std; class Rainbow { public: Rainbow() { cout << "Rainbow()" << endl; } ~Rainbow() { cout << "~Rainbow()" << endl; } }; jmp_buf kansas; void oz() { Rainbow rb; for(int i = 0; i < 3; i++) cout << "there's no place like home\n"; longjmp(kansas, 47); } int main() { if(setjmp(kansas) == 0) { cout << "tornado, witch, munchkins...\n"; oz(); } else { cout << "Auntie Em! " << "I had the strangest dream..." << endl; } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class MyError { const char* const data; public: MyError(const char* const msg = 0) : data (msg) {} }; void f() { // Here we "throw" an exception object: throw MyError("something bad happened"); } int main() { // As you?ll see shortly, // we?ll want a "try block" here: f(); } |
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:
1 2 3 | try { // Code that may generate exceptions } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 | try { // Code that may generate exceptions } catch(type1 id1) { // Handle exceptions of type1 } catch(type2 id2) { // Handle exceptions of type2 } catch(type3 id3) // Etc... } catch(typeN idN) // Handle exceptions of typeN } // Normal execution resumes here |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #include <iostream> using namespace std; class Rainbow { public: Rainbow() { cout << "Rainbow()" << endl; } ~Rainbow() { cout << "~Rainbow()" << endl; } }; void oz() { Rainbow rb; for(int i = 0; i < 3; i++) cout << "there's no place like home\n"; throw 47; } int main() { try { cout << "tornado, witch, munchkins...\n"; oz(); } catch (int) { cout << "Auntie Em! " << "I had the strangest dream..." << endl; } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <iostream> using namespace std; class X { public: class Trouble {}; class Small : public Trouble {}; class Big : public Trouble {}; void f() { throw Big(); } }; int main() { X x; try { x.f(); } catch(X::Trouble&) { cout << "caught Trouble" << endl; // Hidden by previous handler: } catch(X::Small&) { cout << "caught Small Trouble" << endl; } catch(X::Big&) { cout << "caught Big Trouble" << endl; } } |
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:
1 2 3 4 5 | catch(...) { cout << "an exception was thrown" << endl; // Deallocate your resource here, and then re-throw? throw; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <iostream> using namespace std; class Trace { static int counter; int objid; public: Trace() { objid = counter++; cout << "constructing Trace #" << objid << endl; if(objid == 3) throw 3; } ~Trace() { cout << "destructing Trace #" << objid << endl; } }; int Trace::counter = 0; int main() { try { Trace n1; // Throws exception: Trace array[5]; Trace n2; // won't get here } catch(int i) { cout << "caught " << i << endl; } } |
You can see the results in the output of the program:
1 2 3 4 5 6 7 8 | constructing Trace #0 constructing Trace #1 constructing Trace #2 constructing Trace #3 destructing Trace #2 destructing Trace #1 destructing Trace #0 caught 3 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | #include <iostream> using namespace std; class Cat { public: Cat() { cout << "Cat()" << endl; } ~Cat() { cout << "~Cat()" << endl; } }; class Dog { public: void * operator new(size_t sz) { cout << "allocating a Dog" << endl; throw 47; } void operator delete(void * p) { cout << "deallocating a Dog" << endl;::operator delete(p); } }; class UseResources { Cat * bp; Dog * op; public: UseResources(int count = 1) { cout << "UseResources()" << endl; bp = new Cat[count]; op = new Dog; } ~UseResources() { cout << "~UseResources()" << endl; delete[] bp; // Array delete delete op; } }; int main() { try { UseResources ur(3); } catch (int) { cout << "inside handler" << endl; } } |
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:
- Catch exceptions in the constructor and release the resource.
- 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // Safe, atomic pointers #include <iostream> using namespace std; // Simplified. Yours may have other arguments. template < class T, int SZ = 1 > class PWrap { T * ptr; public: class RangeError {}; // Exception class PWrap() { ptr = new T[SZ]; cout << "PWrap constructor" << endl; } ~PWrap() { delete[] ptr; cout << "PWrap destructor" << endl; } T & operator[](int i) throw (RangeError) { if (i >= 0 && i < SZ) return ptr[i]; throw RangeError(); } }; class Cat { public: Cat() { cout << "Cat()" << endl; } ~Cat() { cout << "~Cat()" << endl; } void g() {} }; class Dog { public: void * operator new [](size_t) { cout << "Allocating a Dog" << endl; throw 47; } void operator delete[](void * p) { cout << "Deallocating a Dog" << endl;::operator delete(p); } }; class UseResources { PWrap < Cat, 3 > cats; PWrap < Dog > dog; public: UseResources() { cout << "UseResources()" << endl; } ~UseResources() { cout << "~UseResources()" << endl; } void f() { cats[1].g(); } }; int main() { try { UseResources ur; } catch (int) { cout << "inside handler" << endl; } catch (...) { cout << "inside catch(...)" << endl; } } |
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:
1 2 3 4 5 6 7 8 9 10 | Cat() Cat() Cat() PWrap constructor allocating a Dog ~Cat() ~Cat() ~Cat() PWrap destructor inside handler |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // auto_ptr #include <memory> #include <iostream> using namespace std; class TraceHeap { int i; public: static void * operator new(size_t siz) { void * p = ::operator new(siz); cout << "Allocating TraceHeap object on the heap " << "at address " << p << endl; return p; } static void operator delete(void * p) { cout << "Deleting TraceHeap object at address " << p << endl;::operator delete(p); } TraceHeap(int i): i(i) {} int getVal() const { return i; } }; int main() { auto_ptr < TraceHeap > pMyObject(new TraceHeap(5)); cout << pMyObject -> getVal() << endl; // prints 5 } |
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
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <iostream> using namespace std; double divide(double numerator, double denominator) { try { if (denominator == 0) { throw "Division by zero!"; } return numerator / denominator; } catch (const char* msg) { cerr << msg << endl; throw; } } int main() { double a = 10; double b = 0; double result; try { result = divide(a, b); cout << "Result: " << result << endl; } catch (...) { cerr << "Exception caught!" << endl; } return 0; } |
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:
- 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.
- 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.
- std::bad_alloc: This class is used to indicate errors that occur when the program is unable to allocate memory.
- std::bad_cast: This class is used to indicate errors that occur when a dynamic_cast operation fails.
- 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.
- std::bad_exception signifies an incorrect exception was thrown.
- std::bad_function_call thrown by “null” std::function
- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> #include <stdexcept> void myFunction(int x) { if (x < 0) { throw std::invalid_argument("x must be non-negative"); } std::cout << "x = " << x << std::endl; } int main() { try { myFunction(-1); } catch (const std::exception& ex) { std::cerr << "Caught exception: " << ex.what() << std::endl; } return 0; } |
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:
1 2 3 | void myFunction(int arg) throw (MyException1, MyException2) { // function body } |
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.
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++:
- 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.
- 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.
- 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.
- 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.
- 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.
- Wrap Functions: Wrap functions (especially C library functions) that use ordinary error schemes so they produce exceptions instead.
- 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:
- When writing a library function that must be compatible with an existing interface that uses exception specifications.
- When writing a low-level system function that needs to guarantee that it will not throw exceptions in certain situations.
- 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.