The intent of this article is to provide a quick but complete introduction to C++ exceptions. The exception handling mechanism, common usage schemes and performance issues of exceptions versus return codes will all be covered.
Let's take a look at why we want to at least inform ourselfes about exceptions:
-
Exceptions seperate error handling code from
normal code. This is a good thing, because it
makes the code much more readable. Compare a
long series of
if()instructions, partly for error checking, partly for branching, nested with function calls versus simply writing down the function calls one after another. - Exceptions cannot be ignored accidentally like return values. Althought programmers which are new to exceptions are often confused by this fact, it is far better to notice an error than letting your application continue to run into an undefined state, often causing follow-up errors which make it much harder to find the real cause of a problem.
- Exceptions can carry rich error information, specifying the exact cause of an error and which operations were carried out at that moment. To provide similar features using error codes, you would be required return entire error structures from each function called, or to allocate/deallocate additional error information on the heap.
- It is possible to derive exception classes from each other. This way, you can categorize you errors better, while still making it easy for the user to catch an entire kind of error types (like catching all file access errors, including errors because of locked files, missing files and corrupted files).
- Using exceptions, the return values of your functions can be used for other purposes again. This is important because it enables your functions to be used in initialization lists of class constructors or as arguments to other functions, without the need for temporary variables.
The Problem
The average programmer commonly makes use of return codes to communicate the result of a function call back to the caller. There's nothing wrong with it, but it's just as common that a large portion of these return codes are not checked at all, sometimes because the programmer thought it was unlikely that a specific function might fail just there, sometimes also out of pure lazyness.
If an error goes by undetected, the programm will
be in an undefined state, doing strange things or
crashing in a place where it is next to impossible
to locate the original error which has caused the
instability. To write robust code, all errors have
to be detected and properly handled (or returned to
the caller if they cannot be handled in the current
scope). But doing so requires a lot of grunt work,
littering each and every function with possibly
dozens of if()s.
Think of exceptions as an automation of this
process. When an exception is thrown, the current
function automatically returns to its caller, which
has the option to either catch the exception, or to
ignore it, causing the exception to travel further
upward in the call stack. Once the exception leaves
the main() procedure without being
caught, the program is aborted.
Introduction
Exceptions aren't hard to master. Let's start with an example:
void foo() {
throw 123;
}
int main() {
foo();
std::cout << "here" << std::endl;
}
What will happen here ?
foo() will be called from within
main(). Then, an exception will be
thrown by foo(), travel upwards to the
caller and further, probably causing some kind of
message from your compiler's run time library ;)
Notice that the line of code, directly after the
call to foo(), will not be executed
because the exception smashes back into the
main() function. Next we'll try to
catch the exception, thus preventing it from
aborting our program:
void foo() {
throw 123
}
int main() {
try {
foo();
}
catch(int number) {
std::cout << "exception caught: " << number << std::endl;
}
std::cout << "here" << std::endl;
}
This time, the program won't crash. The exception
we've thrown is going to be caught within
main(), display the message contained
in the catch block and then normally continue
execution after the catch block. Try commenting
out the throw instruction and look what changes when
the program is run.
Of course, a function containing an exception handler can call another function, which also contains an exception handler. Actually, you can even nest exception handlers directly:
int main() {
try {
try {
throw 123;
std::cout << "leaving inner try block normally" << std::endl;
}
catch(int) {
std::cout << "exception caught from inner try block" << std::endl;
throw;
}
std::cout << "leaving outer try block normally" << std::endl;
}
catch(int) {
std::cout << "exception caught in outer try block" << std::endl;
}
std::cout << "program ends" << std::endl;
}
You can see two new things here: A nested try block
which will catch the exception that is thrown
before it reaches the outer try block, and
another way to use the throw
instruction that allows us to re-throw exceptions
inside a catch block: If you write
throw; without any arguments in an
exception handler, the exception that was caught
will be re-thrown.
Try commenting out this line and see what happens!
Exception classes
The C++ standard library provides us with a set of
exception classes, which is used by the C++ library
functions and which we can use in our own
applications. These are made available to your code
by including the stdexcept header.
Some of the exception classes provided by the C++ standard library are:
-
std::exception - Root exception class.
All exception classes in the standard library
are directly or indirectly derived from this
class.
- std::runtime_error - Base class for all errors which are only detectable at runtime, like missing files, out-of-memory situations and the like.
-
std::logic_error - Base class for all
errors which are caused by a programming
error. Reasons for these errors can usually
be permanently resolved. Typical cases are
invalid array indices, uninitialized objects
and more.
-
std::invalid_argument - Derived
from logic_error, this exception is
thrown when an invalid argument is
encountered. Would be the right
exception to throw if your
hex2int()function is called with a string containing the letters "0xabcw" ;) - std::out_of_range - Derived from logic_error, should be thrown if an index passed as an argument to a function or method is not within the valid range (eg. exceeds array boundaries).
-
std::invalid_argument - Derived
from logic_error, this exception is
thrown when an invalid argument is
encountered. Would be the right
exception to throw if your
There are more exception classes in the standard library which you can look up in your compiler's documentation.
There's nothing preventing you from creating your own exception classes, even the seperation of runtime_errors and logic_errors may not be all that useful to you. But let's stick with the standard exception classes for now, so I'll be able to quickly demonstrate the benefits of exception classes.
#include <cmath>
void foo() {
if(std::rand() % 2)
throw std::invalid_argument("I am an invalid_argument error");
else
throw std::out_of_range("I am an out_of_range error");
}
int main() {
try {
foo();
}
catch(const std::logic_error &error) {
std::cout << "exception caught: " << error.what() << std::endl;
}
std::cout << "program ends" << std::endl;
}
Both exceptions (invalid_argument and out_of_range)
will be caught in main() because both
are derived from the class by which we catch the
exception, logic_error. You can catch
an entire category of exceptions, provided the
category shares a common base class by which its
exceptions are classified. Notice that if you
re-throw an exception that is caught as
one of its base classes, it will not become
an actual instance of the base class.
When using exception classes, always throw by value.
Throwing an exception allocated with
new will work, too, but there's no
place where the exception could be safely
deleted again. As shown in the previous
example, exceptions thrown by value can still be
caught by reference, thus not preventing full usage
of polymorphism.
Should you ever need to catch all exceptions, regardless of their type, there's a special argument for catch which allows you to do so:
int main() {
try {
new int[-1];
}
catch(...) {
std::cout << "exception caught" << std::endl;
}
}
In the above example, new throws an exception because the amount of memory we're requesting is invalid. We can catch this exception without knowing its type.
Using exceptions
Now that we know the mechanics of C++ exceptions, let's see how and where we can use them. An exception is thrown, as is indicated by its name, in exceptional situations. Exceptions should not be used to control normal program flow.
However, some programmers understand an exceptional
situation as a situation in which the application
can not continue to run and has to be aborted. If
that was what exceptions were intended for, they
would be nothing more than a fancy, but redundant
implementation of the C runtime's
abort() function.
An exceptional situation is exceptional when the current scope cannot recover from it. For example, a function that is called to load a bitmap, the file name of which being provided as an argument, will reach an exceptional situation when the file does not exist. It can not resolve this error on its own, neither does it know whether this bitmap is unimportant and can be silently replaced by a white square.
ifstream ImageFile(sFilename);
if(!ImageFile)
throw FileAccessException(
string("The file '") + sFilename + "' could not be opened"
);
// ...continue to load the image...
}
For the caller, this situation may not be that exceptional anymore. Maybe the user just selected an invalid file in the file selector and expects all but the entire program aborting, trashing all his previous work. From the programmer's side, this means that when the image is loaded, after being selected by the user, all exceptions relating to file accesses can be handled by displaying a little message box to the user and continuing normal program flow.
string sFilename = showFileSelector();
try {
Image *pNewImage = loadImage(sFilename);
createEditorWindow(auto_ptr<Image>(pNewImage));
}
catch(const FileAccessException &error) {
showMessageBox(string("Error loading image: ") + error.what());
}
}
Throw exceptions when an error (an exceptional situation) occurs which can not be handled in the current scope. This is the most important and also the only rule to remember.
Drawbacks
A place where you would not want to use exceptions is within tight loops that have to run as fast as possible. Even if no exception is thrown, an exception handler can slow down the speed of function calls and object construction a bit. This is because of the stack unwinding code which some compilers will generate. I know there are some programmers which see their entire application as one tight loop which has to run as fast as possible on every single line of code. I can't help those ;)
When refactoring existing code to use exceptions, you have to be very careful not to create situations like this one:
int *pInts = new int[123];
int theNumber = senseOfLife(pInts);
delete []pInts;
if(!theNumber)
reportError("Unable to retrieve sense of life");
}
If the senseOfLife() function would be
modified to throw an exception when it fails to
return the sense of life, the previous snippet of
code would still work, but the exception would
prevent the cleanup code from being run. You can
generate all kinds of awkward behavior from this
exact type of code.
As an example, take the WindowProc()
callback function you use on MS Windows. You can
easily throw an exception in there, catching it with
a try block around the call to
DispatchMessage(). While everything
seems to work as expected on the first sight, your
application's windows will begin to show some
defects when an exception is thrown, like being
unable to click buttons with the mouse because an
internal state of the window manager has not been
reset.
This does not mean that there's an inherent problem with exceptions, just that you should be careful when equipping projects with exception handling that have been built by an exception-unaware programmer using return codes originally.