Exceptions are the way of flagging unexpected conditions or errors that have occurred in C++ program. C++ Language provides a good mechanism to tackle these conditions. The exception mechanism uses three keywords.
- try – It identifies a code block in which an exception can occur
- catch – Identifies a block of code in which the exception is handled
- throw – It causes an exception condition to be originated
Table of Contents
- Introduction to Exception Handling
- Throwing an Exception
- Stack Unwinding in C++
- Unhandled Exceptions – The terminate() and unexpected() Functions
Introduction to Exception Handling
In this tutorial, we will explore C++ exception handling by considering a simple example. Imagine you are writing a program to handle calendar dates and want to ensure that a given year is within a specific range. In this updated example, the program checks whether the provided year is between 1900 and 2024. If the year falls outside this range, a custom exception (DateException
) is thrown, and the error message is printed. The main
function catches the exception, displays the error message, and returns a non-zero exit code. Feel free to modify the year in the f
function to test the exception handling.
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 | #include <iostream> class DateException { const char* err; public: DateException(const char* s) : err(s) {} void print() const { std::cerr << err << std::endl; } }; // A function that operates on dates void g(int year) { if (year < 1900 || year > 2024) { throw DateException("Invalid year"); } // Process date ... } // Some code that uses dates void f() { g(2025); // Try changing the year to test the exception handling } int main() { try { f(); } catch (const DateException& de) { de.print(); return 1; } std::cout << "Program executed successfully." << std::endl; return 0; } |
The basic idea here is that we have a try block:
1 2 3 4 | try { f(); } |
Within this block, we execute some code, in this case a function call f().
Then we have a list of one or more handlers:
1 2 3 4 | catch (DateException de) { de.print(); return 1; } |
If an abnormal condition arises in the code, we can throw an exception:
1 2 | if (year < 1900 || year > 2024) throw DateException("Invalid year"); |
and have it caught by one of the handlers at an outer level, that is, execution will continue at the point of the handler, with the execution stack unwound.
An exception may be a class object type such as DateException, or a fundamental C++ type like an integer. Obviously, a class object type can store and convey more information about the nature of the exception, as illustrated in this example. Saying: throw -37;
We will explore various details of exception handling later, but one general comment is in order. C++ exceptions are not the same as low-level hardware interrupts, nor are they the same as UNIX signals such as SIGTERM. And there’s no linkage between exceptions such as divide by zero (which may be a low-level machine exception) and C++ exceptions.
Throwing an Exception
Throwing an exception transfers control to an exception handler. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <iostream> void f() { throw 37; } void g() { try { f(); } catch (int i) { std::cout << "Caught an integer: " << i << std::endl; } } int main() { g(); return 0; } |
In this example the exception with value 37 is thrown, and control passes to the handler. A throw transfers control to the nearest handler with the appropriate type. “Nearest” means in the sense of stack frames and try blocks that have been dynamically entered.
A class object instance that is thrown is treated similarly to a function argument or operand in a return statement. A temporary copy of the instance may be made at the throw point, just as temporaries are sometimes used with function argument passing. A copy constructor if any is used to initialize the temporary, with the class’s destructor used to destruct the temporary. The temporary persists as long as there is a handler being executed for the given exception. As in other parts of the C++ language, some compilers may be able in some cases to eliminate the temporary. An example:
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 | #include <iostream.h> // Define a custom exception class 'Exc' class Exc { char* s; public: // Constructor to initialize the exception object with an error message Exc(char* e) { s = e; cerr << "ctor called\n"; } // Copy constructor Exc(const Exc& e) { s = e.s; cerr << "copy ctor called\n"; } // Destructor ~Exc() { cerr << "dtor called\n"; } // Getter function to retrieve the error message char* geterr() const { return s; } }; // Function to check a date and throw an exception if it's less than 1900 void check_date(int date) { if (date < 1900) throw Exc("date < 1900"); // Other processing } // Main function int main() { try { // Call the check_date function with a date that triggers an exception check_date(1879); } catch (const Exc& e) { // Catch the exception and print the error message using the getter function cerr << "exception was: " << e.geterr() << "\n"; } return 0; } |
When running this program, you can trace through the various stages of throwing the exception, including the actual throw, making a temporary copy of the class instance, and the invocation of the destructor on the temporary. It’s also possible to have “throw” with no argument, as in:
1 2 3 4 | catch (const Exc& e) { cerr << "exception was: " << e.geterr() << "\n"; throw; } |
What does this mean? It means that rethrowing an exception uses the existing temporary. The rethrown exception is the most recently caught one that hasn’t been fully processed yet. A caught exception is one where the catch clause’s parameter is initialized and the catch clause hasn’t been exited.
In the given example, “throw;” would rethrow the exception represented by “e.” Since there’s no outer catch clause to handle the rethrown exception, a special library function, terminate(), is invoked. If an exception is rethrown and there’s no ongoing exception handling, terminate() is also called.
Stack Unwinding in C++
Exception handling in C++ involves dynamically managing the flow of program execution. When an exception is thrown, the control transfers to the nearest suitable handler within the dynamically surrounding try block. In this context, “nearest” refers to the try block containing a handler that matches the type of the thrown exception. In future discussions, we’ll delve deeper into exception handlers.
When control is transferred to an exception handler, it implies moving from one program context to another. The question arises: what about the cleanup of the old program context? Specifically, what happens to locally allocated class objects? The answer is “yes.” All stack allocated (“automatic”) objects allocated since entering the try block will have their destructors invoked.
Let’s illustrate this with an example:
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 | #include <iostream> class A { int x; public: A(int i) { x = i; cerr << "ctor " << x << endl; } ~A() { cerr << "dtor " << x << endl; } }; void f() { A a1(1); throw "this is a test"; A a2(2); } int main() { try { A a3(3); f(); A a4(4); } catch (const char* s) { cerr << "exception: " << s << endl; } return 0; } |
Output of this program is:
1 2 3 4 5 | ctor 3 ctor 1 dtor 1 dtor 3 exception: this is a test |
In this example, when an exception is thrown in the f
function, control transfers to the catch clause in the main
function. The destructors of the allocated objects (a1
and a3
) are called, while a2
and a4
are not, as they were never allocated.
Note: If class objects contain other objects or arrays with partial construction followed by an exception, only the constructed subobjects will be destructed.
Unhandled Exceptions – The terminate() and unexpected() Functions
In C++, when an exception is thrown but there’s no handler to catch it, a special function called terminate()
is invoked. This occurs when the exception type doesn’t match any available handlers. The terminate()
is a library function that, by default, aborts the program. However, you can override it by providing your own terminate()
function.
Example of overriding terminate()
:
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> #include <cstdlib> typedef void (*PFV)(void); PFV set_terminate(PFV); void custom_terminate() { std::cerr << "terminate() called" << std::endl; std::exit(1); } void throwException() { throw -37; } int main() { set_terminate(custom_terminate); try { throwException(); } catch (char* s) { // Catching a char* exception } return 0; } |
Similarly, the function unexpected()
can be triggered when a function with an exception specification throws an exception of a type not listed in its specification. By default, unexpected()
calls terminate()
, but if the user defines their own version of unexpected()
, execution can continue.
Example of using unexpected()
:
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> #include <cstdlib> typedef void (*PFV)(void); PFV set_unexpected(PFV); void custom_unexpected() { std::cerr << "unexpected() called" << std::endl; std::exit(1); } void throwException() throw(char*) { throw -37; } int main() { set_unexpected(custom_unexpected); try { throwException(); } catch (int i) { // Catching an int exception } return 0; } |
There’s also a new library function called uncaught_exception()
, which returns true during the evaluation of the object to be thrown until the initialization of the exception declaration in the matching handler. If this function returns true, it indicates ongoing stack unwinding, and throwing another exception would lead to the invocation of terminate()
.