Lesson 1: Error Handling

Experienced developers treat error handling as a natural part of the coding process; appropriate error handling is an essential component of a robust application. This lesson explains some of the common techniques used in error handling. This lesson also presents a broad discussion of how to incorporate code that not only detects a problem when it occurs but reacts appropriately (for example, by displaying a message box to users). It's far easier to address error handling during the coding process than to try retrofitting the code later.

After this lesson, you will be able to:

  • Write code that effectively handles errors as they occur in a running program.
  • Understand exceptions and use exception-handling techniques in your code.
  • Keep a running record of a program's progress using the TRACE macro.
Estimated lesson time: 30 minutes

Anticipating Program Errors

Most experienced programmers have a healthy skepticism about whether code will perform as intended. Your code might contain logical errors that affect your program in ways that are not instantly apparent. Even code that is logically error free might have unexpected effects within certain environmental circumstances. As you write code, develop the habit of questioning every assumption. Try to anticipate things that might go wrong and incorporate additional code to react gracefully to such problems. These added lines of code might execute only rarely, if ever, but are present in case they are needed to solve problems.

Failing to properly anticipate environmental contingencies catches many developers off guard. Consider the following typical scenario, in which a function allocates a buffer in memory, then fills it with text. Using the new operator, this function receives a pointer to a block of memory, then immediately proceeds to write to it. Throughout the development process, the function performs flawlessly because the development computer has ample resources to provide free store memory. After the program is released, the program's developer begins receiving calls from irate users who have lost a day's work because the program has failed as a result of the users' computers not having adequate memory to perform the task.

Experienced programmers take a more conservative approach when allocating memory. They add code that checks the value of the pointer returned by new and will only use the pointer if it holds a non-zero value. If new returns a NULL pointer—meaning that the allocation failed—code should react appropriately. The result should resemble the following code:

int  *ptr = new int[BLOCK_SIZE]; if (ptr) {      // Memory block successfully allocated, so use it      .      .      .      delete[] ptr; } else {      // Allocation failed — take appropriate steps }

Anticipation of potential problems and implementation of proper contingency procedures lead to more robust code with increased stability under adverse circumstances, which in turn leads to reduced failure rates. Programmed robustness is considered to be fault-tolerant, referring to the degree to which code can accommodate problems without failure. You might think of this anticipatory approach as inline error handling, in which possible errors are dealt with immediately in the body of the code.

Handling errors by continually checking return values can make code difficult to read and maintain because of constant interruptions of the program's logic flow. This method of error checking can often lead to a long series of nested IF-ELSE blocks, in which the IF blocks contain the code as it is intended to run, and the ELSE blocks contain the code that deals with errors. Following is a pseudocode illustration of how such nested tests can move closer to the right side of the screen. If the nested tests contain long lines of code, the intended flow might be difficult to follow:

if (condition1 == TRUE) {      // Compute condition2      if (condition2 == TRUE)      {           // Compute condition3           if (condition3 == TRUE)           {                // Other nested conditions                .                .                .           }           else           {                // Failure for condition3           }      }      else      {           // Failure for condition2      } } else {      // Failure for condition1 }

Microsoft Windows and Visual C++ offer another approach to handling errors inline: exception handling. Exception handling allows developers to separate a function logically into two sections: one for normal execution and the other for trapping errors. Seemingly oblivious to potential errors, code in the normal execution section does not check return values and executes as though no problems exist. The second code section traps errors as they occur.

Exceptions

An exception is any condition that the operating system considers to be an error. When an application raises an exception, the operating system attempts to notify the offending application that it has caused an error by calling the application's exception handler code (assuming such code exists). If the application does not provide an exception handler, the operating system resolves the problem itself, often by terminating the application abruptly with a terse message to its user such as "This program has performed an illegal operation and will be shut down."

Two levels of exception handling exist:

  • Structured exception handling (SEH) pertains exclusively to operating system errors.
  • C++ exception handling pertains to errors in Visual C++ applications.

Structured Exception Handling

Although this chapter primarily examines C++ exception handling, Structured Exception Handling (SEH) merits attention for two reasons. First, though they are quite distinct, the two types of exception handling are often confused. Second, although the C language cannot use C++ exception handling, it can implement SEH.

All exception handling is based on the SEH mechanism. SEH initiates the be-ginning of a communication chain that winds its way up to the application level. An application incorporates SEH through the __try, __except, and __finally keywords. A __try block must be matched with either an __except block or a __finally block, but not both. The syntax is as follows:

 __try {      // Normal code goes here } __except (filter) {      // Errors that occur in the try block are trapped here }

Note that SEH keywords are preceded by two underscores, not one. The __except block executes only if code in the __try block causes an exception. If the __try block finishes successfully, execution resumes at the next instruc-tion following the __except block, thus bypassing the __except block entirely.

Through its filter parameter, the __except block specifies whether it is able to deal with the exception. The filter parameter must evaluate to one of the values described in Table 13.1.

Table 13.1 Filter Parameter Evaluation Values

Value Meaning
EXCEPTION_CONTINUE_SEARCH The __except block declines the exception and passes control to the handler with the next highest precedence.
EXCEPTION_CONTINUE_EXECUTION The __except block dismisses the exception without ex- ecuting, forcing control to return to the instruction that raised the exception.
EXCEPTION_EXECUTE_HANDLER The body of the __except block executes.

A filter such as EXCEPTION_CONTINUE_EXECUTION might seem odd in that it apparently renders the __except block useless. Why have an exception handler if it never executes, but merely requests that the statement causing the exception be repeated? An __except block usually calls a helper function that returns a value for the filter parameter, as shown in the following code:

__except (GetFilter()) {      // Body of __except block } . . . long GetFilter() {      long lFilter;      // Determine the filter appropriate for the error      return lFilter; }

The helper function must analyze current conditions and attempt to fix the problem that triggered the exception. If it succeeds, the helper function returns EXCEPTION_CONTINUE_EXECUTION, thus causing control to bypass the __except block and return to the original instruction in the __try block for another attempt. However, such techniques must be employed with care; if the helper function does not truly fix the problem, the program enters an infinite loop in which the statement in the __try block continually executes, raising over and over an exception that is never properly solved.

Although nominally part of SEH, the __finally keyword has little to do with the operating system. Rather, it defines a block of instructions that the compiler guarantees will execute when the __try block finishes. Even if the __try block contains a RETURN or GOTO statement that jumps beyond the __finally block, the compiler ensures that the __finally block executes before returning or jumping. The __finally keyword does not take a parameter.

C++ Exception Handling

C++ exception handling is at the higher end of the command chain initiated by SEH. Whereas SEH is a system service, C++ exception handling is code that you write to implement that system service. C++ exception handling is more sophisticated and offers more options than SEH. Use of the low-level __try and __except keywords of SEH is discouraged for Visual C++ applications.

In much the same way that a C application uses SEH, a Visual C++ application provides exception handler code that executes in response to an error. This code resolves the problem and retries the instruction that caused the error, ignores the problem completely, or passes the notification further along the chain of potential handlers. C++ provides the try, catch, and throw keywords for this purpose. Unlike their SEH counterparts, these keywords do not have a double-underscore prefix.

To see a simple example of C++ exception handling, consider again the scenario in which a program attempts to write to unowned memory after the new operator fails. Previously, we saw how an application can prevent failure by testing the value of a returned pointer. Exception handling offers a somewhat more sophisticated solution in which code is separated into try and catch blocks instead of a series of nested IF-ELSE blocks, as demonstrated in the following code:

 try {      int  *iptr = new int[BLOCK_SIZE];       .      .          // If we reach this point, allocation succeeded      .      delete[] iptr; } catch(CMemoryException* e) {      // Allocation failed, so address the problem      e->Delete(); }

If the new command fails to allocate the requested memory, it triggers an exception that causes the catch block to execute. In this example, the catch block accepts as its parameter a pointer to an MFC CMemoryException object, which contains information about out-of-memory conditions caused by the new operator. C++ programs that do not use MFC can design their own class for this purpose, or even use a pointer to a standard type, such as a pointer to a string. The block's parameter list can also be an ellipsis (…), which indicates to the compiler that the catch block handles all types of exceptions, not exclusively memory exceptions.

If the catch block can fix the problem, it retries the instruction that caused the exception by executing the throw command. Notice that this option is much more flexible than using the EXCEPTION_CONTINUE_EXECUTION filter of SEH, because it allows the catch block to execute. If the catch block does not retry (or rethrow) the exception, program flow continues to the next statement following the catch block.

MFC Exception Macros

Early versions of MFC provided macros as substitutes for the C++ exception handling commands, naming them TRY, CATCH, and THROW. For various reasons, the macros have fallen out of favor; since MFC 3.0, these macros have simply become aliases for the original C++ keywords try, catch, and throw. Thus, while MFC still supports the uppercase macro names, the names are no longer recommended because there is no advantage to using them.

Benign Exceptions

Some exceptions are benign and consequently do not show up as errors in an application. For example, a benign exception can occur when a program accesses an uncommitted page of its stack memory. The operating system confronts this situation transparently by trapping the invalid access, committing another page to the stack, and then allowing the access to continue using the committed page. The application does not recognize that the exception has occurred. The only external evidence of such an exception is a momentary delay while the operating system sets up a new page.

Logging Errors

The first two sections of this lesson describe how errors can be handled inline as they occur. Because not all errors can be anticipated and caught by developers, it is important to keep a record of unexpected errors as they occur. This section presents a third technique in which an executing program simply compiles a record of errors (an error log) without stopping to confront them. Developers can then read through the resulting error log after the program terminates, and revise the program's code to ensure that the errors do not occur again.

MFC programs use the TRACE macro and its variations to achieve this "offline" approach to error handling. Many C programmers employ a similar technique by the liberal use of printf() statements throughout their code, which provide a running commentary as the code progresses by displaying brief messages such as these:

 Entering Function1 Allocating memory block Block successfully allocated . . . Leaving Function1

The TRACE macro logs errors by displaying messages at the location specified by AfxDump, which by default is the Debug tab of the Visual C++ Output window. TRACE operates only for a project's debug build (described in Lesson 3 of this chapter). For release builds, the macro does nothing. Because it does not expand into code, the TRACE macro does not increase the size of a program's release version.

TRACE accepts the same string formatting commands as printf(), so you can display variables in a TRACE line as demonstrated in the following code:

 int  iFileSize = 10; char sz[] = "kilobytes"; // Display the string "File size is 10 kilobytes" TRACE("File size is %d %s\n", iFileSize, sz);

The maximum string length after formatting cannot exceed 512 characters, including the terminating NULL. MFC also offers the macros TRACE0, TRACE1, TRACE2, and TRACE3, which take fixed numbers of variables (0, 1, 2 or 3). The only advantage offered by these variations over TRACE is that they expand into code that is a bit more compact.

Lesson Summary

In this lesson, you've learned some of the many ways an application can anticipate and respond to errors as they occur during execution.

The operating system's structured exception handling mechanism is the foundation on which C++ exception handling is built. Visual C++ implements C++ exception handling through the try, catch, and throw keywords. The MFC macro equivalents for these keywords are no longer used in C++ programming.

Testing for return values is an excellent programming habit, but can often be replaced by exception handling. TRACE statements provide a convenient way to create a running record of a program's logic flow, which you can examine to make sure your program is executing as expected.

The philosophy of error handling can be summed up in two words: "Don't assume!" Errors have a habit of occurring when least expected. Proper error-handling techniques enable your program to gracefully respond to the unexpected, without terminating abnormally—or worse, continuing to execute in an unstable state.



Microsoft Press - Desktop Applications with Microsoft Visual C++ 6. 0. MCSD Training Kit
Desktop Applications with Microsoft Visual C++ 6.0 MCSD Training Kit
ISBN: 0735607958
EAN: 2147483647
Year: 1999
Pages: 95

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net