What Are Exceptions?
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, something 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.
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.
The Call Stack and Exceptions
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.
How Do Exceptions Work?
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 main() 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.
// exception, divide-by-0.cpp
// Compile with /clr
#include "stdafx.h"
using namespace System;
int main()
{
Console::WriteLine("Exception Test - divide by 0.");
int top = 3;
int bottom = 0;
int result = top / bottom;
Console::Write("The result is ");
Console::WriteLine(result);
return 0;
}
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 after the JIT Debugger failed (if any).
You can see that the divide-by-zero has resulted in an exception being generated. Because we 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.DivideByZeroException error), presents an error message, and then gives you a stack trace that tells you where the error occurred (in the main() function at line 13 in the Animal1.cpp file). DivideByZeroException 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 Types
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, ref 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. The third sort of exception handling you might encounter is Structured Exception Handling (SEH), a form of exception handling built into Windows operating systems that is independent from C++ (Microsoft extension). We won’t talk any more about SEH here, except to note that you can interact with it from C++.
Throwing Exceptions
We’ll start our exploration of exceptions by discussing how to generate, or throw, them. You’ll end up generating far more exceptions by accident than by design, but you need to know how to generate your own when errors occur in your application.
What Can You Throw?
Traditional C++ lets you attach any type of object to an exception, so you can use built-in types (such as int and double) as well as structures and objects. If you throw objects in C++, you usually throw and catch them by reference. Managed C++ extends this ability to let you throw and catch pointers to managed types, and you’ll most likely be using managed types when you’re writing .NET code. This module deals almost exclusively with throwing and catching managed types, but be aware that you’ll meet other data types that are being used out in the wider C++ world. How do you know what to throw? There are a large number of exception classes as part of the System namespace, all of which derive from Exception. A number of those you’ll commonly encounter are listed in the following table. You should be able to find the exception class to suit your purposes, and if you can’t, it’s always possible to derive your own exception classes from System::Exception.
Exception Class | Description |
System::ApplicationException | Thrown when a non-fatal application error occurs. |
System::ArgumentException | Thrown when one of the arguments to a function is invalid. Subclasses include System::ArgumentNullException and System::ArgumentOutOfRangeException. |
System::ArithmeticException | Thrown to indicate an error in arithmetic, a casting, or a conversion operation. Subclasses include System::DivideByZeroException and System::OverflowException. |
System::Exception | The base class of all exception types. |
System::IndexOutOfRangeException | Thrown when an array index is out of range. |
System::InvalidCastException | Thrown when an invalid cast or conversion is attempted. |
System::MemberAccessException | Thrown when an attempt is made to dynamically access a member that doesn’t exist. Subclasses include System::MissingFieldException and System::MissingMethodException. |
System::NotSupportedException | Thrown when a method is invoked that isn’t supported. |
System::NullReferenceException | Thrown when an attempt is made to dereference a null reference. |
System::OutOfMemoryException | Thrown when memory cannot be allocated. |
System::SystemException | The base class for exceptions that the user can be expected to handle. Subclasses include ArgumentException and ArithmeticException. |
System::TypeLoadException | Thrown when the common language runtime (CLR) cannot find an assembly or a type within an assembly, or cannot load the type. Subclasses include System::DllNotFoundException. |
Table 1 |
The following exercise will show you how to generate an exception. In the next section, you’ll go on to see how to catch and process the exception.
1. Start Microsoft Visual Studio .NET, and create a new CLR Console Application project named Throwing.
2. Immediately after the using namespace System; line and immediately before main(), add the following function definition:
void func(int a)
{
if (a <= 0)
throw gcnew System::ArgumentException("Aaargh! What wrong?");
}
This simple function takes an integer argument, and if its value is less than 0, it throws an exception. In this case, we are creating a new System::ArgumentException object, initializing it with a string, and then throwing it.
3. Add code to test out the function by adding this code to the main() function, replacing the Hello World Console::WriteLine():
Console::WriteLine(L"Throw Test");
Console::WriteLine(L"Calling with a = 3");
func(3);
Console::WriteLine(L"Calling with a = 0");
func(0);
Console::WriteLine(L"All done");
The code calls the function twice, once with a valid value and once with 0, which should trigger the exception. If you are using Visual C++ 7.1/8.0, you need to make one further change before building the program; you don’t need to make this change if you’re using version 7.
4. Use Solution Explorer to open the AssemblyInfo.cpp file
------------------------------------------------------
5. And add the following lines to the file, immediately after the two using namespace lines:
using namespace System::Diagnostics;
[assembly:Debuggable(true, true)];
These lines ensure that full information is generated when the exception is caught. Microsoft changed how exception information is generated between the 7 and 7.1/8.0 releases. By default, the 7.1/8.0 release does not provide full information about where the exception occurred, which is done for efficiency reasons. You can make sure that the full information is generated by adding the Debuggable attribute to AssemblyInfo.cpp, as shown here.
6. Compile and run the code, just click the OK button for the JIT Debugger dialog box and you should get a screen that looks like the following figure.
The program has called the function once without incident, but the second call has triggered an exception. As before, you get a message and a stack trace. This time the message is the string used to initialize the exception object and the stack trace has two levels, showing that the exception was triggered at line 10 in the func function, which was called from the main() function at line 19.
The precise line number you get reported in the exception stack trace will depend on exactly how you typed in and formatted your code.