Exceptions in C++
Fundamentals of C++ Structured Exception Handling
Like many modern languages, especially object-oriented languages, C++ provides a syntax and mechanism for handling runtime errors in a structured way, using a very common “throw-catch” paradigm. The term exception is used as an abstraction for the concept.
PREREQUISITES — You should already…
- have some C or introductory C++ programming experience;
- know how to create and compile C++ programs.
Introduction
Many C++ tutorials and introductory books introduce exception handling at a later stage. The reason for this is because a complete understanding requires prior knowledge about pointers, references and inheritance. However, it is not necessary to be completely conversant with each of those topics. A small overview, enough to help understand exceptions, is presented here to enable learners to start using exception handling in all their code.
Pointers & References
Pointers and references have much in common: at machine code level, they produce the same code. References allows us to write “cleaner” code, since the compiler will automatically take addresses of the initialisers, and apply indirection on use.
Pointers, Indirection and Arithmetic
Pointers are addresses of particular types of values — a pointer type incorporates the type of value it is pointing to, e.g. T*
(read as “T pointer”) means the address of a T
type value. The most common operation on a pointer expression (which results in an address value), is indirection, which represents a value at that address.
Pointers also enable another kind of arithmetic, called “pointer arithmetic”: this states that it is legal to add or subtract an integer type value to or from a pointer expression. Given any expression E
, which has type T*
, then the result of E+/-I
, where I
is an integer type value, will be calculated as: the value-of
(E) +/- I * sizeof(T)
.
References
References are “disguised” pointers. This means that, at assembler level, they work exactly like pointers, but at source code level, this relationship is hidden — it is recognised and treated as a reference, when the pattern T&
(read as “T reference”) is encountered.
When the type of a variable, member, element, argument or function return, is a reference type, it means that during initialisation, the address of each initialiser is automatically taken, and whenever the variable, member, element, parameter or function return is used, indirection is applied automatically.
In C++11, we not only have lvalue references (T&
), we also find rvalue references (T&&
), which allow for a concept called move semantics, involving move constructors and overloaded move operators. This has no effect on understanding exceptions, however, so we will not elaborate on that here.
Pointer & Reference Conversions
Pointers can be explicitly cast to any other pointer type, using either the C-style cast: (T*)E
, or preferably, using: reinterpret_cast<T>(E)
, or static_cast<T>(E)
in some instances. They are not automatically converted to any other pointer, except to void*
(and one other scenario, which we will investigate later).
Reference types can, by the same rule, be cast to other reference types, but not to void&
, which is not a valid type: reinterpret_cast<T&>(E)
.
Inheritance
Classes can inherit from one or more base classes. One interesting and extremely useful effect of this feature, is that derived class pointers are implicitly cast to their base class pointers, when used to initialise, assign, pass, or return, a base class pointer.
Since references are really pointers, this also applies to derived class references. Derived class references are automatically converted to their base class references, when used in a base class reference context — initialisation, assignment, argument passing and function returns.
The benefit of this can be seen when we write, for example, a function that takes a base class reference or pointer, as parameter. Now we can, without explicit type conversion, pass it either a base class, or a derived class. Even if we pass a derived class pointer or reference, the only accessible members are those that were inherited from the base class, which is still extremely useful.
Thread of Execution
We can visualise the step-wise execution of a program as a thread that is fixed at the start of a program's startup code: it unwinds with a call to main()
; then unwinds ever more as statements are executed. Calls to functions also unwind this proverbial thread, and calls to other functions within them, unwind it even further — to an arbitrary depth. Normally, only return
statements from functions can rewind the thread back up again. This trail that the thread leaves, is recorded on the stack. This is why this rewinding of the thread is often referred to as “unwinding the stack”, because the stack size decreases (it really should have been called “rewinding the stack”).
Exception handling can use this trail and follow it right back to the start, if necessary. In the process of retracing the thread, the exception handling mechanism will look for a handler in the current function. If not found, it will perform normal local variable clean-up, before it rewinds the thread to the caller, at which time it repeats the process, until it potentially reaches main()
. If still not caught in main()
, the program will terminate with an “unhandled exception” message.
Throwing Exceptions
Instead of having functions return error codes, we can throw
an exception. An exception is simply an expression (object) of any type, used as part of the throw
‹
expression›;
statement. The expression of the throw
statement can be any type, but it is not considered good practice to “throw” intrinsic or common library data types.
To reiterate, the statement: throw 123;
(int
object) and throw "‹message›";
(throwing a char*
object) are both legal, but frowned upon. On the other hand, the statement:
throw std::runtime_error("‹message›");
is encouraged, since it uses a type from the <stdexcept>
header file, which is more meaningful than int
or char*
.
Inherit Exception Type
It is important, from the perspective of good programming practice, that the exception's type indicates:
- that it is an exception, and
- what kind of error was encountered.
Although a little too simplistic for production code, an empty structure is often used, just to create a name that indicates an error:
struct negative_radius_error { };
...
if (radius < 0.0)
throw negative_radius_error{};
It is common to rather inherit from one of the standard exceptions in <stdexcept>
, in particular: std::runtime_error
. For example:
#include <stdexcept>
class negative_radius_error : std::runtime_error {
public: // ctors
(const char* message)
negative_radius_error : std::runtime_error(message)
{ }
};
...
if (radius < 0.0)
throw negative_radius_error{"Radius negative in assignment"};
The main benefit of this, is that the class inherits the virtual what()
method. Exception handlers can call this method to retrieve the error message
created in the throw
during construction.
There is another version of throw
, which takes no expression after it, and is only legal inside catch
blocks (discussed later): throw;
. Mechanically, this merely continues the process of rewinding the trail thread (or unwinding the stack), as before.
Note that from C++11, the canonical way to inherit a custom ‘error type’ is as follows:
#include <stdexcept>
class negative_radius_error : std::runtime_error {
public: // inheritc ctors (c++11 and later only)
using std::runtime_error::runtime_error;
};
...
if (radius < 0.0)
throw negative_radius_error{"Radius negative in assignment"};
This will automatically inherit the constructors of std::runtime_error
, which can be called as if constructors for negative_radius_error
.
Standard Library Exceptions
All exceptions in the C++ standard library inherit from std::exception
, the declaration of which can be found in <exception>
. A very common standard exception, is the one thrown by the new
operator: std::bad_alloc
from the <new>
header.
Although exceptions are thrown throughout the standard library, with classes and functions declared in several header files, exceptions are all declared in <stdexcept>
(std::exception
is the exception).
Try and Catch
Handlers for exceptions are only searched for in code that is specially marked with a try
block. The “block” is basically a compound statement. Any number of catch
blocks, which contain the exception handler code, can be attached to the try
block (physically following it).
Each catch block accepts one argument. If multiple catch blocks are used on the same try
block, each of the parameter types must be different. Optionally, a catch
block can be specified to catch any exception, of any type, using ellipses (...
) as parameter specification.
NOTE — Catch Parameters
Although the syntax does not enforce it, the types of the catch
parameters should be a reference type. This prevents unnecessary copying of the thrown object.
Care must be taken with the order of the catch
blocks, because the handler-finding code does not look for the best match; it only looks for any legal match, starting from the top. This means that, if the first catch
has a parameter of type std::exception&
, and a second catch
block has a parameter of type std::bad_alloc&
(which derives from std::exception
), the second block will never be reached. The solution is simply to list catch
blocks with the most derived exception parameters first, before the ones with base class reference parameters.
// good version
try { … }
catch (std::bad_alloc& ex) { … }
catch (std::runtime_error& ex) { … }
catch (std::exception& ex) { … }
catch (...) { … }
// bad version
try { … }
catch (std::exception& ex) { … }
catch (std::runtime_exception& ex) { … } // will never be reached
catch (std::bad_alloc& ex) { … } // will never be reached
catch (...) { … }
The body of a function, even constructors, destructors and overloaded operators, can consist in its entirety of a try
block, followed by one or more of its associated catch blocks.
functryblock.cpp
— Function Try Block Example
/*!@file functryblock.cpp
* @brief Function Try Block Example
*/
#include <iostream>
#include <stdexcept>
int main ()
try{
// do main's work.
return EXIT_SUCCESS;
}
catch (std::bad_alloc& ex) {
std::cerr << "Memory issues. Terminating." << std::endl;
return EXIT_FAILURE;
}
catch (std::exception& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
return EXIT_FAILURE;
}
catch (...) {
std::cerr << "Unusual exception." << std::endl;
return EXIT_FAILURE;
}
All exceptions that inherit from std::exception
, inherit the what()
method, which returns a C-style string (const char*
). This contains the message thrower code considered useful, at the time it constructed the thrown exception object.
Rethrow Caught Exceptions
Inside catch
blocks, and only inside them, a rethrow can be effected with: ‘throw;
’. This will continue the search for a handler in the caller function, as if the exception was not caught. This is useful when we want to catch an exception, in order to do some clean-up or logging, but we do not want to handle it in the current function.
Unhandled Exceptions
When an exception is not handled, i.e., “escapes” main()
, the runtime calls a standard library function called std::terminate()
. A program can take control of this function, by passing a function pointer to std::set_terminate()
. Only one function can be set as the termination function. A pointer to the previous terminate function is subsequently returned from the std::set_terminate()
function. The function passed can also be a lambda, but it cannot be a functor. The value of the current function pointer can be obtained with std::get_terminate()
.
IMPORTANT — Deprecated Function
The std::unexpected()
family of functions is deprecated since C++11, so avoid using them in new projects.
Final or Sentinel Code
Languages that use memory managers (garbage collectors) to manage memory on the programmer's behalf, generally have an optional clause to the try…catch
statement. It is often called finally
. The purpose of this construct is to provide a way to designate “code that is guaranteed to run before the function returns, whether an exception was thrown, caught, or not”.
These languages need such a construct, because destruction of objects is not deterministic — the garbage collector will eventually destruct the objects and reclaim the memory. In C++, we simply have to create a local object within a scope, with destructor code that runs when execution reaches the end of the scope — guaranteed. As long as we can specify the “finally” code that runs, we have in fact more flexibility in when it is executed.
Simplistic Method
The following code simply creates a local struct
type (it could have been a class
), and a variable of this type, at the same time. The destructor for the type (~finally()
) represents the “finally” code that must run at the end of the current scope — regardless of how the scope was exited: return
, throw
, or catch
.
finally_demo1.cpp
— A Simple “Finally” Pattern
/*!@file finally_demo1.cpp
* @brief Illustration of Simple “Finally” Code
*/
#include <iostream>
using std::cout; using std::cerr; using std::endl;
extern void demo_func(); // function containing “finally” code example.
int main () {
try{
<< "main(): entering `try` block." << endl;
cout ();
demo_func<< "main(): after `demo_func()`." << endl;
cout }
catch (...) {
<< "main(): exception caught." << endl; // not reached
cerr }
return EXIT_SUCCESS;
}
// the destructor below is the “finally” code, which will be called
// at the end of the scope in which the `finally` variable has been
// defined — in this case, at the end of the function (*after* the
// last `cout`, in fact). it should not cause any exceptions to be
// thrown, or exceptions to escape its body (`noexcept`).
//
void demo_func() {
struct finally { ~finally() noexcept { // “finally” pattern
<< "\t<< FINALLY CODE >>" << endl; // ...
cerr }} finally{}; // ...
try{
<< "demo_func(): try block entered." << endl;
cout throw 123; // arbitrary exception
}
catch (...) {
<< "demo_func(): exception caught." << endl;
cerr }
<< "demo_func(): returning." << endl;
cout }
In the example above, the “finally” code is run after the last statement in the function — just before the machine code instructions resume after the call to main()
. This is controlled by considering the scope in which the finally
variable has been created. If we surround the struct
finally…
up to the line before the "…returning."
message with curly braces (making it a compound statement), the code will then run before the last statement.
If an exception was thrown in the try
block, as above, the code will still execute, but after the code in the catch
block has been executed. If you want the code to execute before the catch
block, simply move the type declaration and finally
variable definition into the try
block. Now it will execute before the code in the exception block.
The “finally” pattern above could be moved to the end of the function, even to after the last statement, as long as it is not a return
statement — and it will still execute as explained. Where in the scope it is placed, is therefore immaterial, since it will still be destructed at the end of the scope.
Preprocessor Finally Pattern
Although not favoured by C++ programmers, the preprocessor can nevertheless produce cleaner code, as evidenced by Qt with its own “pre-preprocessor” (moc
). As long as the preprocessor code is not obscure, we have no real problem with it.
finally_demo2.cpp
— A “Finally” Pattern with a Macro
/*!@file finally_demo2.cpp
* @brief Illustration of Simple “Finally” Code with a Macro
*/
#include <iostream>
using std::cout; using std::cerr; using std::endl;
extern void demo_func(); // function containing “finally” code example.
int main () {
try{
<< "main(): entering `try` block." << endl;
cout ();
demo_func<< "main(): after `demo_func()`." << endl;
cout }
catch (...) {
<< "Exception caught" << endl; // not reached
cerr }
return EXIT_SUCCESS;
}
// this macro would typically be placed in a header file and included.
#define FINALLY(code) \
struct finally { ~finally() noexcept { \
code \
}} finally{}
// since the macro creates a `finally` variable, only one `FINALLY()`
// can appear in a single scope. nested scopes are fine, as shown.
//
void demo_func() {
(
FINALLY<< "\t<< FINALLY CODE (1) >>" << endl;
cerr << "\t<< MORE FINALLY (1) >>" << endl;
cerr ); // runs at function return.
try{
(
FINALLY<< "\t<< FINALLY CODE (2) >>" << endl;
cerr << "\t<< MORE FINALLY (2) >>" << endl;
cerr ); // runs at end of `try{…}`
<< "demo_func(): try block entered." << endl;
cout throw 123; // arbitrary exception
}
catch (...) {
<< "demo_func(): exception caught." << endl;
cerr }
<< "demo_func(): returning." << endl;
cout }
There is no question — it is very clean and convenient, as long as you remember the admonitions: only one FINALLY
per scope. But that is seldom a problem.
Lambda Template Finally
The full power of C++ can be used to implement a “finally” pattern, using templates and lambdas. Lambdas, of course, are only available since C++11. The _finally
template below implements good programming conventions, and uses static_assert
, for example, to ensure that the “finally” code promises to throw no exceptions. Also, by deleting some constructors, copying of the “finally” object is not permitted. Liberal use of rvalue references and std::move()
make this very safe.
Those lines containing static_cast<void>(…)
at the end of some scopes, illustrate a convention which reminds readers that some code will execute at the end of that scope. Furthermore, it suppresses “unused variable” warnings from the compiler.
finally_demo3.cpp
— A “Finally” Pattern with Templates and Lambdas
/*!@file finally_demo3.cpp
* @brief Illustration of Simple “Finally” Code with a Template
*/
#include <iostream>
using std::cout; using std::cerr; using std::endl;
extern void demo_func(); // function containing “finally” code example.
int main () {
try{
<< "main(): entering `try` block." << endl;
cout ();
demo_func<< "main(): after `demo_func()`." << endl;
cout }
catch (...) {
<< "main(): exception caught." << endl; // not reached
cerr }
return EXIT_SUCCESS;
}
// the following templates would normally be in a header file. the only
// client-usable identifier below, is the `finally()` template function.
template <typename F> struct _finally;
template <typename F> auto finally(F code) -> _finally<F> {
return { std::move(code) };
}
template <typename F> struct _finally {
() = delete;
_finally(_finally&&) = delete;
_finally(const _finally&) = delete;
_finally& operator=(const _finally&) = delete;
_finally(F func) : finally_code_{std::move(func)} {}
_finally~_finally() noexcept {
static_assert(noexcept(finally_code_()),
"Lambda must not throw exceptions; use `noexcept`");
if (!cancel_flag_) try {finally_code_();} catch(...) {}
}
void cancel() { cancel_flag_ = true; }
void enable() { cancel_flag_ = false; }
private:
finally_code_;
F bool cancel_flag_ = false;
};
void demo_func() {
<< "\ndemo_func(): start." << endl;
cout auto&& final1{finally([]() noexcept {
<< "\t<< FINALLY CODE 1 >>" << endl;
cerr << "\t<< MORE FINALLY 1 >>" << endl;
cerr })};
// compound statement to create a scope
{
auto&& final2{finally([]() noexcept {
<< "\t<< FINALLY CODE 2 >>" << endl;
cerr << "\t<< MORE FINALLY 2 >>" << endl;
cerr })};
try {
auto&& final3{finally([]() noexcept {
<< "\t<< FINALLY CODE 3 >>" << endl;
cerr << "\t<< MORE FINALLY 3 >>" << endl;
cerr })};
<< "demo_func(): code in the `try`" << endl;
cout auto&& final4{finally([]() noexcept {
<< "\t<< FINALLY CODE 4 >>" << endl;
cerr << "\t<< MORE FINALLY 4 >>" << endl;
cerr })};
<< "demo_func(): more code in `try`" << endl;
cout .cancel();
final3throw 123;
static_cast<void>(final3);
static_cast<void>(final4);
}
catch(...) {
<< "demo_func(): exception caught" << endl;
cerr }
static_cast<void>(final2);
}
static_cast<void>(final1);
<< "demo_func(): returning." << endl;
cout }
This is clearly more sophisticated than the previous solutions. It allows for an arbitrary number of “finally” objects, and the finally lambda code can use variables from the outer function (as long as they have been specified in the capture clause — between the square brackets of the lambda). Furthermore, it can be arbitrarily cancelled or enabled via the member functions cancel()
and enable()
respectively. It does require a more complicated setup, and although it might not look as clean, it is nevertheless the recommended pattern to use.
NOTE — Scope of “Finaliser”
If you move the definition of, for example, final4
, to after the throw 123;
statement, it will never be initialised, and therefore never be destructed. This is guaranteed, and if that is the behaviour you want, there is nothing wrong with it. Of course, it would be pointless in the simplistic example above, but the throw
could appear after a conditional statement, for example, and then the concept would be perfectly viable.
And as a final reminder: the “finally” code is executed when it goes it out of scope. It has nothing to do with the presence, or absence, of try…catch
statements. You could use the concept to execute any code, guaranteed, at the end of any scope, even the compound statement following iteration or selection statements. Or simply at the end of a function body block. It should, however, be used sparingly.
Summary
Handling exceptions are statements, and just like other statements containing statements, (e.g., if()
, while()
, for()
, etc.), can be nested. And just like these other statements, there is no one correct way to use try-catch
statements. It is part of program design to decide where and how to deal with which exceptions. Generally, however, we try to write exception-safe code, especially in lower level, helper functions, and handle errors higher up the calling chain.
Sometimes we have no choice other than to throw exceptions. Constructors, for example, can only report errors by throwing exceptions. Regardless, many library methods throw (documented) exceptions, and they cannot be ignored, so you should catch them, and deal with the problems.
2021-10-20: Added paragraph about C++11 constructor inheritance and example. [brx]
2019-12-04: Changed ‘explicit’ to ‘implicit’. [brx]
2019-02-19: Added reference to the <new>
header. [brx]
2017-11-18: Update to new admonitions. [brx]
2016-09-23: Created. [brx]