Exceptions are an error-handling mechanism employed extensively in C++ and several other modern programming languages. Traditionally, error and status information is passed around using function return values and parameters, like this:
// Pass status back as return value bool bOK = doSomething(); // Pass status back in a parameter int status; doSomething(arg1, arg2, &status);
Although this is a tried and tested way of passing status information around, it suffers from several drawbacks:
You can’t force the programmer to do anything about the error.
The programmer doesn’t even have to check the error code.
If you’re deep down in a series of nested calls, you have to set each status flag and back out manually.
It’s very difficult to pass back status information from something that doesn’t take arguments or return a value.
Exceptions provide an alternative error-handling mechanism, which gives you three main advantages over traditional return value error handling:
Exceptions can’t be ignored. If an exception isn’t handled at some point, the program will terminate, which makes exceptions suitable for handling critical errors.
Exceptions don’t have to be handled at the point where the exception occurs. An error can occur at many levels of a function calls deep in a program, and there might not be a way to fix the problem at the point where the error occurs. Exceptions let you handle the error anywhere up the call stack. (See the following sidebar “The Call Stack and Exceptions.”)
Exceptions provide a useful way to signal errors where a return value can’t be used. There are two particular places in C++ where return values can’t be used: constructors don’t use them, and overloaded operators can’t have their return value overloaded to use for error and status information. Exceptions are particularly useful in these situations because they let you sidestep the normal return-value mechanism.
At any point in a program, the call stack holds information about which functions have been called to get to the current point. The call stack is used in three main ways by programs: during execution to control calling and returning from functions, by the debugger, and during exception handling.
The handler for an exception can occur in the routine in which the exception was thrown. It can also occur in any routine above it in the call stack, and, at run time, each routine in the call stack is checked to see if it implements a suitable handler. If nothing suitable has been found by the time the top of the stack has been reached, the program terminates.
In .NET, exceptions have one other significant advantage: they can be used across languages. Because exceptions are part of the underlying .NET Framework, it’s possible to throw an exception in C++ code and catch it in Microsoft Visual Basic .NET, something that isn’t possible outside the .NET environment.
As is the case with any other error mechanism, you’ll tend to trigger exceptions by making errors in your code. However, you can also generate exceptions yourself if necessary, as you’ll see shortly.
When an error condition occurs, the programmer can generate an exception using the throw keywords and the exception is tagged with a piece of data that identifies exactly what has happened. At this point, normal execution stops and the exception-handling code built into the program goes to look for a handler. It looks in the currently executing routine, and if it finds a suitable handler, the handler is executed and the program continues. If no handler is found in the current routine, the exception-handling code moves one level up the call stack and checks for a suitable handler. This process carries on until either a handler is found or the top level in the call stack—the _tmain function—is reached. If nothing has been found by this time, the program is terminated with an unhandled exception message.
Here’s an example of how an unhandled exception appears to you. You’ve probably seen a lot of these already! Look at the following simple code fragment:
Console::WriteLine("Exception Test"); int top = 3; int bottom = 0; int result = top / bottom; Console::Write("Result is "); Console::WriteLine(result);
It’s easy to see that this code is going to cause a divide-by-zero error, and when it is executed, you see the result shown in the following figure.
You can see that the divide-by-zero has resulted in an exception being generated. Because I didn’t handle it in the code, the program has been terminated and the final output never makes it to the screen. Notice the form of the standard message: it tells you what happened (a System.DivideByZero error), presents an error message, and then gives you a stack trace that tells you where the error occurred (in the _tmain function at line 22 in the first.cpp file).
System.DivideByZero denotes the kind of object that was passed in the exception. A lot of exception classes are provided in the System namespace, and it’s also likely that you’ll make up your own, based on the System::Exception base class, as you’ll see later.
Exception handling is slightly complicated in that you might encounter three different types of exception handling when using managed C++: traditional C++ exceptions, managed C++ exceptions, and Microsoft Windows Structured Exception Handling (SEH). Traditional C++ exceptions form the basis of all exception handling in C++. Managed C++ adds the ability to use managed types (for example, __gc classes and __value types) in exceptions, and you can mix them with traditional exceptions. Managed C++ also extends exception handling by adding the concept of a __finally block, which I’ll discuss in the section “The __finally Block” later in the chapter. The third sort of exception handling you might encounter is SEH, a form of exception handling built into Windows operating systems that is independent from C++. I won’t talk any more about SEH here, except to note that you can interact with it from C++.