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.
So why are exceptions a good thing?
-
Exceptions separate 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
conditions, 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 like return values. Although programmers 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 program 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:
#include <iostream>
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:
#include <iostream>
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:
#include <iostream>
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-throw
n.
Try commenting out this line and see what happens!
Exception classes
The standard C++ 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 yourhex2int()
function is called with a string containing the letters “0xabcw” ;) -
std::out_of_range
– Derived fromstd::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).
-
-
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_error
s and logic_error
s 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 <iostream>
#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 delete
d 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:
#include <iostream>
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.
Image *loadImage(const string &sFilename) {
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.
void onFileOpen() {
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:
void foo() { 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 find the sense of life, the previous snippet of code would still work, but the exception would prevent the cleanup code from being run. Apart from memory leaks, you can generate all kinds of awkward behavior from this exact type of code – try throwing an exception throughWindowProc()
and catch it in your message pump for a taste :)
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.