Day 20. Handling Errors and Exceptions

The code you’ve seen in this book has been created for illustration purposes. It has not dealt with errors so that you would not be distracted from the central issues being presented. Real-world programs must take error conditions into consideration.

Today, you will learn

• What exceptions are

• How exceptions are used and what issues they raise

• How to build exception hierarchies

• How exceptions fit into an overall error-handling approach

• What a debugger is

Bugs, Errors, Mistakes, and Code Rot

It is rare for a real-world-sized program not to have some sort of error, or bug. The bigger the program, the more likely there will be bugs. In fact, in larger programs, it is often the case that many bugs actually “get out the door” and into final, released software. That this is true does not make it okay. Making robust, bug-free programs should be the number-one priority of anyone serious about programming.

The single biggest problem in the software industry is buggy, unstable code. One of the biggest expenses in many major programming efforts is testing and fixing. The person who solves the problem of producing good, solid, bulletproof programs at low cost and on time will revolutionize the software industry.

A number of discrete kinds of errors can trouble a program. The first is poor logic: The program does just what you asked, but you haven’t thought through the algorithms properly. The second is syntactic: You used the wrong idiom, function, or structure. These two are the most common, and they are the ones most programmers are on the lookout for.

Research and real-world experience have shown that the later in the development process you find a logic problem, the more it costs to fix it. The least expensive problems or bugs to fix are the ones you manage to avoid creating. The next cheapest are those spotted by the compiler. The C++ standards force compilers to put a lot of energy into making more and more bugs show up at compile time.

Errors that get compiled in your program, but are caught at the first test—those that crash every time—are less expensive to find and fix than those that are flaky and only crash once in a while.

A more common runtime problem than logic or syntactic bugs is fragility: Your program works just fine if the user enters a number when you ask for one, but it crashes if the user enters letters. Other programs crash if they run out of memory, if the floppy disk is left out of the drive, or if an Internet connection is lost.

To combat this kind of fragility, programmers strive to make their programs bulletproof. A bulletproof program is one that can handle anything that comes up at runtime, from bizarre user input to running out of memory.

It is important to distinguish between bugs, which arise because the programmer made a mistake; logic errors, which arise because the programmer misunderstood the problem or how to solve it; and exceptions, which arise because of unusual but predictable problems such as running out of resources (memory or disk space).

Exceptional Circumstances

You can’t eliminate exceptional circumstances; you can only prepare for them. What happens if your program requests memory to dynamically allocate an object, and there isn’t any available? How will your program respond? Or, what will your program do if you cause one of the most common math errors by dividing by zero? Your choices include

• Crash.

• Inform the user and exit gracefully.

• Inform the user and allow the user to try to recover and continue.

• Take corrective action and continue without disturbing the user.

Consider Listing 20.1, which is extremely simple and ready to crash; however, it illustrates a problem that makes it into many programs and that is extremely serious!

Listing 20.1. Creating an Exceptional Situation

0:  // This program will crash
1:  #include <iostream>
2:  using namespace std;
3:
4:  const int DefaultSize = 10;
5:
6:  int main()
7:  {
8:     int top = 90;
9:     int bottom = 0;
10:  
11:     cout << "top / 2 = " << (top/ 2) << endl;
12:   
13:     cout << "top divided by bottom = ";
14:     cout << (top / bottom) << endl;
15:  
16:     cout << "top / 3 = " << (top/ 3) << endl;
17:  
18:    cout << "Done." << endl;
19:    return 0;
20: }

Image


top / 2 = 45
top divided by bottom =

Caution

This program might display the preceding output to the console; however, it will most likely immediately crash afterward.

Image

Listing 20.1 was actually designed to crash; however, if you had asked the user to enter two numbers, he could have encountered the same results.

In lines 8 and 9, two integer variables are declared and given values. You could just as easily have prompted the user for these two numbers or read them from a file. In lines 11, 14, and 16, these numbers are used in math operations. Specifically, they are used for division. In lines 11 and 16, there are no issues; however, line 14 has a serious problem. Division by zero causes an exceptional problem to occur—a crash. The program ends and most likely an exception is displayed by the operating system.

Although it is not always necessary (or even desirable) to automatically and silently recover from all exceptional circumstances, it is clear that you must do better than this program. You can’t simply let your program crash.

C++ exception handling provides a type-safe, integrated method for coping with the predictable but unusual conditions that arise while running a program.

The Idea Behind Exceptions

The basic idea behind exceptions is fairly straightforward:

• The computer tries to run a piece of code. This code might try to allocate resources such as memory, might try to lock a file, or any of a variety of tasks.

• Logic (code) is included to be prepared in case the code you are trying to execute fails for some exceptional reason. For example, you would include code to catch any issues, such as memory not being allocated, a file being unable to be locked, or any of a variety of other issues.

• In case your code is being used by other code (for instance, one function calling another), you also need a mechanism to pass information about any problems (exceptions) from your level, up to the next. There should be a path from the code where an issue occurs to the code that can handle the error condition. If intervening layers of functions exist, they should be given an opportunity to clean the issue but should not be required to include code whose only purpose is to pass along the error condition.

Exception handling makes all three of these points come together, and they do it in a relatively straightforward manner.

The Parts of Exception Handling

To handle exceptions, you have to first identify that you want a particular piece of code to be watched for any exceptions. This is accomplished by using a try block.

You should create a try block around any area of code that you believe has the potential to cause a problem. The basic format of the try block is:


try
{
     SomeDangerousFunction();
}
catch (...)
{
}

In this case, when SomeDangerousFunction() executes, if any exception occurs, it is noted and caught. Adding the keyword try and the braces is all that is required to have your program start watching for exceptions. Of course, if an exception occurs, then you need to act upon it.

When the code within a try block is executed, if an exception occurs, the exception is said to be “thrown.” Thrown exceptions can then be caught, and as shown previously, you catch an exception with a catch block! When an exception is thrown, control transfers to the appropriate catch block following the current try block. In the previous example, the ellipse (...) refers to any exception. But you can also catch specific types of exceptions. To do this, you use one or more catch blocks following your try block. For example,


try
{
     SomeDangerousFunction();
}
catch(OutOfMemory)
{
     // take some actions
}
catch(FileNotFound)
{
     // take other action
}
catch (...)
{
}

In this example, when SomeDangerousFunction() is executed, there will be handling in case there is an exception. If an exception is thrown, it is sent to the first catch block immediately following the try block. If that catch block has a type parameter, like those in the previous example, the exception is checked to see if it matches the indicated type. If not, the next catch statement is checked, and so on, until either a match is found or something other than a catch block is found. When the first match is found, that catch block is executed. Unless you really intended to let other types of exceptions through, it is always a good idea to have the last catch use the ellipse parameter.

Note

A catch block is also called a handler because it can handle an exception.

Note

You can look at the catch blocks as being like overloaded functions. When the matching signature is found, that function is executed.

The basic steps in handling exceptions are

1. Identify those areas of the program in which you begin an operation that might raise an exception, and put them in try blocks.

2. Create catch blocks to catch the exceptions if they are thrown. You can either create a catch for a specific type of exception (by specifying a typed parameter for the catch block) or all exceptions (by using an ellipses (...) as the parameter).

Listing 20.2 adds basic exception handling to Listing 20.1. You can see this with the use of both a try block and a catch block.

Note

Some very old compilers do not support exceptions. Exceptions are part of the ANSI C++ standard, however, and every compiler vendor’s latest edition fully supports exceptions. If you have an older compiler, you won’t be able to compile and run the exercises in today’s lesson. It’s still a good idea to read through the entire chapter, however, and return to this material when you upgrade your compiler.

Listing 20.2. Catching an Exception


0:  // trying and catching
1:  #include <iostream>
2:  using namespace std;
3:
4:  const int DefaultSize = 10;
5:
6:  int main()
7:  {
8:     int top = 90;
9:     int bottom = 0;
10:
11:     try
12:     {
13:        cout << "top / 2 = " << (top/ 2) << endl;
14:
15:        cout << "top divided by bottom = ";
16:        cout << (top / bottom) << endl;
17:
18:        cout << "top / 3 = " << (top/ 3) << endl;
19:     }
20:     catch(...)
21:     {
22:        cout << "something has gone wrong!" << endl;
23:     }
24:
25:     cout << "Done." << endl;
26:     return 0;
27:  }

Image


top / 2 = 45
top divided by bottom = something has gone wrong!
Done.

Image

Unlike the prior listing, executing Listing 20.2 doesn’t cause a crash. Rather, the program is able to report an issue and exit gracefully.

This time, a try block was added around the code where a potential issue could occur. In this case, it is around the division operations (lines 11 to 19). In case an exception does occur, a catch block is included in lines 20–23 after the try block.

The catch on line 20 contains three dots, or an ellipsis. As mentioned previously, this is a special case for catch, and indicates that all exceptions that occur in the preceding try’s code should be handled by this catch statement, unless a prior catch block handled the exception. In this listing, that will most likely only be a division by zero error. As you will see later, it is often better to look for more specific types of exceptions so that you can customize the handling of each.

You should notice that this listing does not crash when it is run. In addition, you can see from the output that the program continued to line 25 right after the catch statement. This is confirmed by the fact that the word “Done" was printed to the console.

try Blocks

A try block is a series of statements that begins with the keyword try; it is followed by an opening brace and ends with a closing brace.

Example


try
{
     Function();
};

catch Blocks

A catch block is a piece of code that begins with the keyword catch, followed by an exception type in parentheses, followed by an opening brace, and ending with a closing brace. catch blocks are only allowed to follow a try block.

Example


try
{
     Function();
};
catch (OutOfMemory)
{
     // take action
}

Causing Your Own Exceptions

Listing 20.2 illustrated two of the aspects of exception handling—marking the code to be watched and specifying how the exception is to be handled. However, only predefined exceptions were handled. The third part of exception handling is the ability for you to create your own types of exceptions to be handled. By creating your own exceptions, you gain the ability to have customized handlers (catch blocks) for exceptions that are meaningful to your application.

To create an exception that causes the try statement to react, the keyword, throw, is used. In essence, you throw the exception and, hopefully, a handler (catch block) catches it. The basic format of the throw statement is:


throw exception;

With this statement, exception is thrown. This causes control to be passed to a handler. If a handler can’t be found, the program terminates.

The value that you throw in the exception can be of virtually any type. As mentioned earlier, you can set up corresponding handlers for each different type of object your program might throw. Listing 20.3 illustrates how to throw a basic exception by modifying Listing 20.2.

Listing 20.3. Throwing an Exception

Image

Image


top / 2 = 45
top divided by bottom = *** Division by zero! ***
Done.

Image

Unlike the prior listing, this listing takes more control of its exceptions. Although this isn’t the best use of exceptions, it clearly illustrates using the throw statement.

In line 17, a check is done to see if the value of bottom is equal to zero. If it is, an exception is thrown. In this case, the exception is a string value.

On line 24, a catch statement starts a handler. This handler is looking for a constant character pointer. With exceptions, strings are matched to a constant character pointer, so the handler starting in line 24 catches the throw in line 18. In line 26, the string that was passed is displayed between asterisks. Line 27 is the closing brace, which indicates the end of the handler, so control goes to the first line following the catch statements and the program continues to the end.

If your exception had been a more serious problem, you could have exited the application after printing the message in line 26. If you throw your exception in a function that was called by another function, you could have passed the exception up. To pass on an exception, you can simply call the throw command without any parameter. This causes the existing exception to be rethrown from the current location.

Creating an Exception Class

You can create much more complex classes for throwing an exception. Listing 20.4 presents a somewhat stripped-down Array class, based on the template developed on Day 19, “Templates.”

Listing 20.4. Throwing an Exception

Image

Image

Image

Image


intArray[0] okay...
intArray[1] okay...
intArray[2] okay...
intArray[3] okay...
intArray[4] okay...
intArray[5] okay...
intArray[6] okay...

intArray[7] okay...
intArray[8] okay...
intArray[9] okay...
intArray[10] okay...
intArray[11] okay...
intArray[12] okay...
intArray[13] okay...
intArray[14] okay...
intArray[15] okay...
intArray[16] okay...
intArray[17] okay...
intArray[18] okay...
intArray[19] okay...
Unable to process your input!
Done.

Image

Listing 20.4 presents a somewhat stripped-down Array class; however, this time exception handling is added in case the array goes out of bounds.

On line 24, a new class, xBoundary, is declared within the declaration of the outer class Array.

This new class is not in any way distinguished as an exception class. It is just a class the same as any other. This particular class is incredibly simple; it has no data and no methods. Nonetheless, it is a valid class in every way.

In fact, it is incorrect to say it has no methods because the compiler automatically assigns it a default constructor, destructor, copy constructor, and the assignment operator (operator equals); so it actually has four class functions, but no data.

Note that declaring it from within Array serves only to couple the two classes together. As discussed on Day 16, “Advanced Class Relationship,” Array has no special access to xBoundary, nor does xBoundary have preferential access to the members of Array.

On lines 63–70 and 72–79, the offset operators are modified to examine the offset requested, and if it is out of range, to throw the xBoundary class as an exception. The parentheses are required to distinguish between this call to the xBoundary constructor and the use of an enumerated constant.

In line 90, the main part of the program starts by declaring an Array object that can hold 20 values. On line 91, the keyword try begins a try block that ends on line 98. Within that try block, 101 integers are added to the array that was declared on line 90.

On line 99, the handler has been declared to catch any xBoundary exceptions.

In the driver program on lines 88–105, a try block is created in which each member of the array is initialized. When j (line 93) is incremented to 20, the member at offset 20 is accessed. This causes the test on line 66 to fail, and operator[] raises an xBoundary exception on line 67.

Program control switches to the catch block on line 99, and the exception is caught or handled by the catch on the same line, which prints an error message. Program flow drops through to the end of the catch block on line 102.

Placing try Blocks and catch Blocks

Figuring out where to put your try blocks can be hard: It is not always obvious which actions might raise an exception. The next question is where to catch the exception. It might be that you’ll want to throw all memory exceptions where the memory is allocated, but you’ll want to catch the exceptions high in the program where you deal with the user interface.

When trying to determine try block locations, look to where you allocate memory or use resources. Other things to look for are out-of-bounds errors, illegal input, and so forth. At the very least, put a try/catch around all of the code in main(). try/catch usually belongs in high-level functions, particularly those that know about the program’s user interface. For instance, a utility class should not generally catch exceptions that need to be reported to the user because it might be used in windowed programs or console programs, or even in programs that communicate with users via the Web or messaging.

How Catching Exceptions Work

Here’s how it works: When an exception is thrown, the call stack is examined. The call stack is the list of function calls created when one part of the program invokes another function.

The call stack tracks the execution path. If main() calls the function Animal::GetFavoriteFood(), and GetFavoriteFood() calls Animal::LookupPreferences(), which, in turn, calls fstream::operator>>(), all these are on the call stack. A recursive function might be on the call stack many times.

The exception is passed up the call stack to each enclosing block. This is called “unwinding the stack.” As the stack is unwound, the destructors for local objects on the stack are invoked, and the objects are destroyed.

One or more catch statements follow each try block. If the exception matches one of the catch statements, it is considered to be handled by having that statement execute. If it doesn’t match any, the unwinding of the stack continues.

If the exception reaches all the way to the beginning of the program (main()) and is still not caught, a built-in handler is called that terminates the program.

It is important to note that the exception unwinding of the stack is a one-way street. As it progresses, the stack is unwound and objects on the stack are destroyed. There is no going back: After the exception is handled, the program continues after the try block of the catch statement that handled the exception.

Thus, in Listing 20.4, execution continues on line 101, the first line after the try block of the catch statement that handled the xBoundary exception. Remember that when an exception is raised, program flow continues after the catch block, not after the point where the exception was thrown.

Using More Than One catch Specification

It is possible for more than one condition to cause an exception. In this case, the catch statements can be lined up one after another, much like the conditions in a switch statement. The equivalent to the default statement is the “catch everything” statement, indicated by catch(...). Listing 20.5 illustrates multiple exception conditions.

Listing 20.5. Multiple Exceptions

Image

Image

Image

Image


You asked for an array of zero objects!
Done.

Image

Four new classes are created in lines 25–29: xTooBig, xTooSmall, xZero, and xNegative. In the constructor, on lines 56–71, the size passed to the constructor is examined. If it’s too big, too small, negative, or zero, an exception is thrown.

The try block is changed to include catch statements for each condition other than negative, which is caught by the “catch everything” statement catch(...), shown on line 101.

Try this with a number of values for the size of the array. Then try putting in –5. You might have expected xNegative to be called, but the order of the tests in the constructor prevented this: size < 10 was evaluated before size < 1. To fix this, swap lines 61 and 62 with lines 65 and 66 and recompile.

Tip

After the constructor has been invoked, memory has been allocated for the object. Therefore, throwing any exception from the constructor can leave the object allocated but unusable. Generally, you should wrap the constructor in a try/catch, and if an exception occurs, mark the object (internally) as unusable. Each member function should check this “valid” flag to be certain additional errors won’t occur when someone uses an object whose initialization was interrupted.

Exception Hierarchies

Exceptions are classes, and as such, they can be derived from. It might be advantageous to create a class xSize, and to derive from it xZero, xTooSmall, xTooBig, and xNegative. Thus, some functions might just catch xSize errors, and other functions might catch the specific type of xSize error. Listing 20.6 illustrates this idea.

Listing 20.6. Class Hierarchies and Exceptions

Image

Image

Image

Image


This array is too small...
Done.

Image

The significant change is on lines 27–30, where the class hierarchy is established. Classes xTooBig, xTooSmall, and xNegative are derived from xSize, and xZero is derived from xTooSmall.

The Array is created with size zero, but what’s this? The wrong exception appears to be caught! Examine the catch block carefully, however, and you will find that it looks for an exception of type xTooSmall before it looks for an exception of type xZero. Because an xZero object is thrown and an xZero object is an xTooSmall object, it is caught by the handler for xTooSmall. After being handled, the exception is not passed on to the other handlers, so the handler for xZero is never called.

The solution to this problem is to carefully order the handlers so that the most specific handlers come first and the less specific handlers come later. In this particular example, switching the placement of the two handlers xZero and xTooSmall fixes the problem.

Data in Exceptions and Naming Exception Objects

Often, you will want to know more than just what type of exception was thrown so you can respond properly to the error. Exception classes are the same as any other class. You are free to provide data, initialize that data in the constructor, and read that data at any time. Listing 20.7 illustrates how to do this.

Listing 20.7. Getting Data Out of an Exception Object

Image

Image

Image

Image

Image


This array is too small...
Received 9
Done.

Image

The declaration of xSize has been modified to include a member variable, itsSize, on line 33 and a member function, GetSize(), on line 31. In addition, a constructor has been added that takes an integer and initializes the member variable, as shown on line 29.

The derived classes declare a constructor that does nothing but initialize the base class. No other functions were declared, in part to save space in the listing.

The catch statements on lines 113–135 are modified to name the exception they catch, theException, and to use this object to access the data stored in itsSize.

Note

Keep in mind that if you are constructing an exception, it is because an exception has been raised: Something has gone wrong, and your exception should be careful not to kick off the same problem. Therefore, if you are creating an OutOfMemory exception, you probably don’t want to allocate memory in its constructor.

It is tedious and error-prone to have each of these catch statements individually print the appropriate message. This job belongs to the object, which knows what type of object it is and what value it received. Listing 20.8 takes a more object-oriented approach to this problem, using virtual functions so that each exception “does the right thing.”

Listing 20.8. Passing by Reference and Using Virtual Functions in Exceptions

Image

Image

Image

Image

Image


Too small! Received: 9
Done.

Image

Listing 20.8 declares a virtual method on lines 33–37 in the xSize class, PrintError(), that prints an error message and the actual size of the class. This is overridden in each of the derived classes.

On line 141 in the exception handler, the exception object is declared to be a reference. When PrintError() is called with a reference to an object, polymorphism causes the correct version of PrintError() to be invoked. The code is cleaner, easier to understand, and easier to maintain.

Exceptions and Templates

When creating exceptions to work with templates, you have a choice: You can create an exception for each instance of the template, or you can use exception classes declared outside the template declaration. Listing 20.9 illustrates both approaches.

Listing 20.9. Using Exceptions with Templates

Image

Image

Image

Image

Image


Bad Size!
Done.

Image

The first exception, xBoundary, is declared outside the template definition on line 4. The second exception, xSize, is declared from within the definition of the template on line 28.

The exception xBoundary is not tied to the template class, but it can be used the same as any other class. xSize is tied to the template and must be called based on the instantiated Array. You can see the difference in the syntax for the two catch statements. Line 105 shows catch (xBoundary), but line 109 shows catch (Array<int>::xSize). The latter is tied to the instantiation of an integer Array.

Exceptions Without Errors

When C++ programmers get together for a virtual beer in the cyberspace bar after work, talk often turns to whether exceptions should be used for routine conditions. Some maintain that by their nature, exceptions should be reserved for those predictable but exceptional circumstances (hence the name!) that a programmer must anticipate, but that are not part of the routine processing of the code.

Others point out that exceptions offer a powerful and clean way to return through many layers of function calls without danger of memory leaks. A frequent example is this: The user requests an action in a graphical user interface (GUI) environment. The part of the code that catches the request must call a member function on a dialog manager, which, in turn, calls code that processes the request, which calls code that decides which dialog box to use, which, in turn, calls code to put up the dialog box, which finally calls code that processes the user’s input. If the user clicks Cancel, the code must return to the very first calling method where the original request was handled.

One approach to this problem is to put a try block around the original call and catch CancelDialog as an exception, which can be raised by the handler for the Cancel button. This is safe and effective, but clicking Cancel is a routine circumstance, not an exceptional one.

This frequently becomes something of a religious argument, but a reasonable way to decide the question is to ask the following: Does use of exceptions in this way make the code easier or harder to understand? Are there fewer risks of errors and memory leaks, or more? Will it be harder or easier to maintain this code? These decisions, like so many others, require an analysis of the trade-offs; no single, obvious right answer exists.

A Word About Code Rot

Code rot is a well-known phenomenon in which software deteriorates due to being neglected. A perfectly well-written, fully debugged program will turn bad on your customer’s shelf just weeks after you deliver it. After a few months, your customer will notice that a green mold has covered your logic, and many of your objects have begun to flake apart.

Besides shipping your source code in air-tight containers, your only protection is to write your programs so that when you go back to fix the spoilage, you can quickly and easily identify where the problems are.

Note

Code rot is a programmer’s joke, which teaches an important lesson. Programs are enormously complex, and bugs, errors, and mistakes can hide for a long time before turning up. Protect yourself by writing easy-to-maintain code.

This means that your code must be written to be understood, and commented where tricky. Six months after you deliver your code, you will read it with the eyes of a total stranger, bewildered by how anyone could ever have written such convoluted and twisty logic.

Bugs and Debugging

Nearly all modern development environments include one or more high-powered debuggers. The essential idea of using a debugger is this: You run the debugger, which loads your source code, and then you run your program from within the debugger. This enables you to see each instruction in your program as it executes and to examine your variables as they change during the life of your program.

All compilers let you compile with or without symbols. Compiling with symbols tells the compiler to create the necessary mapping between your source code and the generated program; the debugger uses this to point to the line of source code that corresponds to the next action in the program.

Full-screen symbolic debuggers make this chore a delight. When you load your debugger, it reads through all your source code and shows the code in a window. You can step over function calls or direct the debugger to step into the function, line by line.

With most debuggers, you can switch between the source code and the output to see the results of each executed statement. More powerfully, you can examine the current state of each variable, look at complex data structures, examine the value of member data within classes, and look at the actual values in memory of various pointers and other memory locations. You can execute several types of control within a debugger that include setting breakpoints, setting watch points, examining memory, and looking at the assembler code.

Breakpoints

Breakpoints are instructions to the debugger that when a particular line of code is ready to be executed, the program should stop. This allows you to run your program unimpeded until the line in question is reached. Breakpoints help you analyze the current condition of variables just before and after a critical line of code.

Watch Points

It is possible to tell the debugger to show you the value of a particular variable or to break when a particular variable is read or written to. Watch points enable you to set these conditions, and, at times, even to modify the value of a variable while the program is running.

Examining Memory

At times, it is important to see the actual values held in memory. Modern debuggers can show values in the form of the actual variable; that is, strings can be shown as characters, longs as numbers rather than as four bytes, and so forth. Sophisticated C++ debuggers can even show complete classes and provide the current value of all the member variables, including the this pointer.

Assembler

Although reading through the source can be all that is required to find a bug, when all else fails, it is possible to instruct the debugger to show you the actual assembly code generated for each line of your source code. You can examine the memory registers and flags, and generally delve as deep into the inner workings of your program as required.

Learn to use your debugger. It can be the most powerful weapon in your holy war against bugs. Runtime bugs are the hardest to find and squash, and a powerful debugger can make it possible, if not easy, to find nearly all of them.

Summary

Today, you learned the basics for creating and using exceptions. Exceptions are objects that can be created and thrown at points in the program where the executing code cannot handle the error or other exceptional condition that has arisen. Other parts of the program, higher in the call stack, implement catch blocks that catch the exception and take appropriate action.

Exceptions are normal, user-created objects, and as such can be passed by value or by reference. They can contain data and methods, and the catch block can use that data to decide how to deal with the exception.

It is possible to create multiple catch blocks, but after an exception matches a catch block’s signature, it is considered to be handled and is not given to the subsequent catch blocks. It is important to order the catch blocks appropriately so that more specific catch blocks have first chance, and more general catch blocks handle those not otherwise handled.

Today’s lesson also mentioned the fundamentals of symbolic debuggers, including using watch points, breakpoints, and so forth. These tools can help you zero in on the part of your program that is causing the error and let you see the value of variables as they change during the course of the execution of the program.

Q&A

Q   Why bother with raising exceptions? Why not handle the error right where it happens?

A   Often, the same error can be generated in different parts of the code. Exceptions let you centralize the handling of errors. In addition, the part of the code that generates the error might not be the best place to determine how to handle the error.

Q   Why generate an object? Why not just pass an error code?

A   Objects are more flexible and powerful than error codes. They can convey more information, and the constructor/destructor mechanisms can be used for the creation and removal of resources that might be required to properly handle the exceptional condition.

Q   Why not use exceptions for nonerror conditions? Isn’t it convenient to be able to express-train back to previous areas of the code, even when nonexceptional conditions exist?

A   Yes, some C++ programmers use exceptions for just that purpose. The danger is that exceptions might create memory leaks as the stack is unwound and some objects are inadvertently left in the free store. With careful programming techniques and a good compiler, this can usually be avoided. Otherwise, it is a matter of personal aesthetic; some programmers feel that, by their nature, exceptions should not be used for routine conditions.

Q   Does an exception have to be caught in the same place where the try block created the exception?

A   No, it is possible to catch an exception anywhere in the call stack. As the stack is unwound, the exception is passed up the stack until it is handled.

Q   Why use a debugger when I can use cout and other such statements?

A   The debugger provides a much more powerful mechanism for stepping through your code and watching values change without having to clutter your code with thousands of debugging statements. In addition, there is a significant risk each time you add or remove lines from your code. If you have just removed problems by debugging, and you accidentally delete a real code line when deleting your use of cout, you haven’t helped the situation.

Workshop

The Workshop contains quiz questions to help solidify your understanding of the material covered and exercises to provide you with experience in using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix D, and be certain you understand the answers before going to tomorrow’s lesson.

Quiz

1. What is an exception?

2. What is a try block?

3. What is a catch statement?

4. What information can an exception contain?

5. When are exception objects created?

6. Should you pass exceptions by value or by reference?

7. Will a catch statement catch a derived exception if it is looking for the base class?

8. If two catch statements are used, one for base and one for derived, which should come first?

9. What does catch(...) mean?

10. What is a breakpoint?

Exercises

1. Create a try block, a catch statement, and a simple exception.

2. Modify the answer from Exercise 1, put data into the exception along with an accessor function, and use it in the catch block.

3. Modify the class from Exercise 2 to be a hierarchy of exceptions. Modify the catch block to use the derived objects and the base objects.

4. Modify the program from Exercise 3 to have three levels of function calls.

5. BUG BUSTERS: What is wrong with the following code?

Image

This listing shows exception handling for handling an out-of-memory error.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.66.94