Structured Exception Handling vs. C Exception Handling

[Previous] [Next]

Getting up to speed on exception handling can be tough partly because C++ can use two main types of exception handling: structured exception handling (SEH), which the operating system provides, and C++ exception handling, which the C++ language provides. Just figuring out which type of exception handling to use when can be a challenge, and it doesn't help that many people talk about both types as if they were interchangeable. I assure you that each type of exception handling has a distinctly different approach. I think what confuses some people is that you can combine both types. In the following sections, I'll touch on the differences and similarities between these two types of exception handling. I'll also describe how to combine them in a way that will let you avoid some of the problems that can crop up when you use them together.

Structured Exception Handling

The operating system provides SEH, and it deals directly with crashes such as access violations. SEH is language-independent but is usually implemented in C and C++ programs with the __try/__except and __try/__finally keyword pairs. The way you use the __try/__except pair is to set your code inside a __try block and then determine how to handle the exception in the __except block (also called an exception handler). In a __try/__finally pair, the __finally block (also called a termination handler) ensures that a section of code will always be executed upon leaving a function, even if the code in the __try block terminates prematurely.

Listing 9-1 shows a typical function with SEH. The __except block looks almost like a function call, but the parentheses specify the value of a special expression called an exception filter. The exception filter value in Listing 9-1 is EXCEPTION_EXECUTE_HANDLER, which indicates that the code in the __except block must be executed every time any exception occurs inside the __try block. The two other possible exception filter values are EXCEPTION_CONTINUE_EXECUTION, which allows an exception to be ignored, and EXCEPTION_CONTINUE_SEARCH, which passes the exception up the call chain to the next __except block. You can make the exception filter expression as simple or as complicated as you like so that you target only those exceptions you're interested in handling.

Listing 9-1 Example SEH handler

void Foo ( void ) { __try { __try { // Execute code to accomplish something. } __except ( EXCEPTION_EXECUTE_HANDLER ) { // This block will be executed if the code in the // block causes an access violation or some other hard crash. // The code in here is also called the exception handler. } } __finally { // This block will be executed regardless of whether the function // causes a crash. Mandatory cleanup code goes here. } } 

The process of finding and executing an exception handler is sometimes called unwinding the exception. Exception handlers are kept on an internal stack; as the function call chain grows, the exception handler (if one exists) for each new function is pushed onto this internal stack. When an exception occurs, the operating system finds the thread's exception handler stack and starts calling the exception handlers until one exception handler indicates that it will handle the exception. As the exception works its way down the exception handler stack, the operating system cleans up the call stack and executes any termination handlers it finds along the way. If the unwinding continues to the end of the exception handler stack, the Application Error dialog box pops up.

Your exception handler can determine the exception value by calling the special GetExceptionCode function, which can be called only in exception filters. If you were writing a math package, for example, you might have an exception handler that handles divide-by-zero attempts and returns NaN (not a number). The code in Listing 9-2 shows an example of such an exception handler. The exception filter calls GetExceptionCode, and if the exception is divide-by-zero, the exception handler executes. If any other exception occurs, EXCEPTION_CONTINUE_SEARCH tells the operating system to execute the next __except block up the call chain.

Listing 9-2 Example SEH handler with exception filter processing

long IntegerDivide ( long x , long y ) { long lRet ; __try { lRet = x / y ; } __except ( EXCEPTION_INT_DIVIDE_BY_ZERO == GetExceptionCode ( ) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH ) { lRet = NaN ; } return ( lRet ) ; } 

If your exception filter requires more complexity, you can even call one of your own functions as the exception filter as long as it specifies how to handle the exception by returning one of the valid exception filter values. In addition to calling the special GetExceptionCode function, you can also call the GetExceptionInformation function in the exception filter expression. GetExceptionInformation returns a pointer to an EXCEPTION_POINTERS structure that completely describes the reason for a crash and the state of the CPU at that time. You might have guessed that the EXCEPTION_POINTERS structure will come in handy later in this chapter.

SEH isn't limited just to handling crashes. You can also create your own exceptions with the RaiseException API function. Most developers don't use RaiseException, but it does offer a way to quickly leave deeply nested conditional statements in code. The RaiseException exit technique is cleaner than using the old setjmp and longjmp run-time functions.

Before you dive in and start using SEH, you need to be aware of two of its limitations. The first is minor: your custom error codes are limited to a single unsigned integer. The second limitation is a little more serious: SEH doesn't mix well with C++ programming because C++ exceptions are implemented with SEH and the compiler complains when you try to combine them indiscriminately. The reason for the conflict is that when straight SEH unwinds out of a function, it doesn't call any of the C++ object destructors for objects created on the stack. Because C++ objects can do all sorts of initialization in their constructors, such as allocating memory for internal data structures, skipping the destructors can lead to memory leaks and other problems.

If you'd like to learn more about SEH, I recommend two references in addition to perusing the Microsoft Developer Network (MSDN). The best overview of SEH is in Jeffrey Richter's Programming Applications for Microsoft Windows (Microsoft Press, 1999). If you're curious about the actual SEH implementation, check out Matt Pietrek's article "A Crash Course on the Depths of Win32 Structured Exception Handling" in the January 1997 Microsoft Systems Journal.

C++ Exception Handling

Because C++ exception handling is part of the C++ language specification, it's probably more familiar to most programmers than SEH. The keywords for C++ exception handling are try and catch. The throw keyword allows you to initiate an exception unwind. Whereas SEH error codes are limited to just a single unsigned integer, a C++ catch statement can handle any variable type, including classes. If you derive your error handling classes from a common base class, you can handle just about any error you need to in your code. This class hierarchy approach to error handling is exactly what the Microsoft Foundation Class (MFC) library does with its CException base class. Listing 9-3 shows C++ exception handling in action with an MFC CFile class read.

Listing 9-3 C++ exception handler example

BOOL ReadFileHeader ( CFile * pFile , LPHEADERINFO pHeader ) { ASSERT ( FALSE == IsBadReadPtr ( pFile , sizeof ( CFile * ) ) ) ; ASSERT ( FALSE == IsBadReadPtr ( pHeader , sizeof ( LPHEADERINFO ) ) ) ; if ( ( TRUE == IsBadReadPtr ( pFile , sizeof ( CFile * ) ) ) || ( TRUE == IsBadReadPtr ( pHeader , sizeof ( LPHEADERINFO ) ) ) ) { return ( FALSE ) ; } BOOL bRet ; try { pFile->Read ( pHeader , sizeof ( HEADERINFO ) ) ; bRet = TRUE ; } catch ( CFileException * e ) { // If the header couldn't be read because the file was // truncated, handle it; otherwise, continue the unwind. if ( CFileException::endOfFile == e->m_cause ) { e->Delete(); bRet = false; } else { // The throw keyword just by itself throws the same exception // as passed to this catch block. throw ; } } return ( bRet ) ; } 

You need to keep in mind the following drawbacks when you're using C++ exception handling: First, it doesn't handle your program crashes automatically. (Later in the chapter, I'll show you how you can work around this limitation.) Second, C++ exception processing isn't free. The compiler might do a great deal of work setting up and removing the try and catch blocks even if you never throw any exceptions, so if you're working on performance-sensitive code you might not be able to afford that much overhead. Although these performance-sensitive cases are rare, they do occur. If you're new to C++ exception handling, MSDN is a great place to start learning about it.

Combining SEH and C++ Exception Handling

As mentioned earlier, there is a way to combine both SEH and C++ exceptions; the result is that you have to use only C++ exception handling in your code. The C run-time library function _set_se_translator lets you set a translator function that will be called when a structured exception happens so that you can throw a C++ exception. This powerful function is a hidden gem. The following code snippet shows all that a translator function must do:

void SEHToCPPException ( UINT uiEx , EXCEPTION_POINTERS * pExp ) { // CSEHException is a class derived from the MFC CException // class. throw CSEHException ( uiEx , pExp ) ; } 

The first parameter is the SEH code returned through a call to GetExceptionCode. The second parameter is the exception state from a call to GetExceptionInformation.

When using the _set_se_translator function in your code, you should catch exception classes thrown out of your translator function only if you're expecting a crash. For example, if you allow users to extend your application with DLLs, you can wrap the calls to the DLLs with try...catch blocks to handle potential crashes. In the course of normal processing, however, you should end the application when you get a hard SEH crash. In one of my own programs, I accidentally handled an access violation instead of just crashing. As a result, instead of leaving the user's data alone, I proceeded to wipe out her data files.

The reason you should be careful about handling hard SEH exceptions as C++ exceptions is that the process is in an unstable state. You can show dialog boxes and write out crash information to a file as part of your handling. However, you need to be aware that the stack might be blown and so you don't have the room to call many functions. Because the exception code is passed to your translator function, you need to check it for EXCEPTION_STACK_OVERFLOW and degrade your error handling gracefully if there is insufficient stack space.

As you can see in the preceding code snippet that translates the SEH exception into the C++ exception, you can throw any class you'd like. Implementing the exception class is trivial; the interesting part is in translating the EXCEPTION_POINTERS information into human-readable form. Before delving into that code, though, I want to explain asynchronous and synchronous C++ exception handling.

Asynchronous vs. Synchronous C++ Exception Handling

When using C++ exception handling, you must understand the difference between asynchronous and synchronous exception handling. Unfortunately, the words asynchronous and synchronous don't adequately describe the difference between these two types of C++ exception handling. The real difference between asynchronous and synchronous C++ exception handling comes down to this distinction: how the compiler assumes that exceptions will be thrown dictates how the compiler generates the exception handling code for the program.

In asynchronous C++ exception handling, the compiler assumes that each instruction could generate an exception and that the code must be prepared to handle the exceptions anywhere. The default exception model for Visual C++ 5 was asynchronous exception handling. The problem with asynchronous exception handling is that the compiler has to track the lifetime of objects and be prepared to unwind the exceptions at any point in the code. All the additional code generated can be substantial, and the resulting code bloat is a waste because the extra code often isn't needed.

With synchronous exception handling, the default for Visual C++ 6, the compiler expects you to throw exceptions only with an explicit throw statement. Thus the compiler doesn't have to track the lifetime of an object and doesn't have to generate the code needed to handle the unwinding if the object's lifetime doesn't overlap a function call or a throw statement. You can think of asynchronous as "all functions track lifetimes of objects" and synchronous as "some functions track lifetimes of objects."

The impact of the switch to synchronous exception handling as the default is that in your release builds you can end up in situations in which your carefully constructed _set_se_translator function never gets called, your code doesn't catch the translated exception, and your application crashes as a normal application would. The default /GX switch maps to /EHsc (synchronous exceptions), so to turn on asynchronous exceptions, you need to explicitly use /EHa (asynchronous exceptions). Fortunately, you don't have to enable asynchronous exceptions projectwide—you can compile different source files with different exception handling and link them together without problems.

If you want to compile with asynchronous exception handling, /EHa, but without the overhead of the object lifetime tracking on functions that don't throw exceptions, you can use __declspec(nothrow) to declare or define those functions. Although you have to do more work by manually declaring __declspec(nothrow), you reap the benefits of _set_se_translator and tighter code.

The code in Listing 9-4 shows a program using _set_se_translator that doesn't work if compiled as a release build with the default synchronous exceptions. The code must be compiled with /EHa. In your programs, if you want to ensure that you can use _set_se_translator anywhere, including in functions outside classes, you must compile with /EHa and take the hit of the extra code overhead. If you have a C++ program, especially one written using MFC, you might get by with using synchronous exceptions if the only places you'll be using the class thrown by your _set_se_translator function are in member functions.

Listing 9-4 An example in which synchronous exceptions don't work

 // Compile as a Win32 Release configuration using /GX to see that the // translator function won't be called. /GX maps to /EHsc. To make // this program work in a release build, compile with /EHa. #include "stdafx.h" class CSEHError { public : CSEHError ( void ) { m_uiErrCode = 0 ; } CSEHError ( unsigned int u ) { m_uiErrCode = u ; } ~CSEHError ( void ) { } unsigned int m_uiErrCode ; } ; void TransFunc ( unsigned int u , EXCEPTION_POINTERS * pEP ) { printf ( "In TransFunc\n" ) ; throw CSEHError ( u ) ; } void GrungyFunc ( char * p ) { *p = 'p' ; printf ( "This output should never be seen!\n" ) ; } void DoBadThings ( void ) { try { GrungyFunc ( (char*)0x1 ) ; } catch ( CSEHError e ) { printf ( "Got an exception! -> 0x%08X\n" , e.m_uiErrCode ) ; } } int main ( int argc, char* argv[] ) { _set_se_translator ( TransFunc ) ; DoBadThings ( ) ; return 0; } 



Debugging Applications
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2000
Pages: 122
Authors: John Robbins

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