A Visual C Exception FAQ

来源:百度文库 编辑:神马文学网 时间:2024/05/01 14:39:47

A Visual C++ Exception FAQ

Copyright © 2001-2007 Doug Harrison

This document answers some common questions concerning catch(...) andexceptions in general as implemented by Visual C++. It's structured mainly as aconversation, in which one question and answer leads to the next, so you'll getthe most out of it if you read it as a whole. To give you a quick idea of whatI'm going to talk about, the questions are:

Q1   I wrote the following, and I don't understand why catch(...) doesn't catch the Win32 structured exception in a release build, or in general, when compiling with optimizations (e.g. /O1 or /O2). Q2 I also wrote the code above, and I don't understand why the Win32 structured exception (SE) is caught in a debug build or when I compile with /EHa. Also, I sometimes find that catch(...) catches SEs even in release builds when I use /GX. Isn't catch(...) supposed to catch only C++ exceptions? Q3 So what are the consequences of catching Win32 Structured Exceptions (SEs) in catch(...) ? Q4 What about _set_se_translator? Q5 How do I deal with all this? Q6 How do I safely use _com_error, std::exception, and other non-MFC exception classes in MFC programs?

This document applies to Visual C++ 5 through Visual C++ .NET 2003 andbeyond. The upcoming Visual C++ 2005 release, known also as "Whidbey", correctsone of the problems discussed below, and this partly affects questions Q1, Q2,and Q5, which are updated accordingly. The remaining questionsand answers apply in full to Visual C++ 5 and later.

Q1. I wrote the following, and I don't understand why catch(...)doesn't catch the Win32 structured exception in a release build, or in general, when compiling withoptimizations (e.g. /O1 or /O2).

#include int main(){   try   {     int* p = 0;     *p = 0; // Cause access violation   }   catch (...)   {      puts("Caught access violation");   }   return 0;}

A. In Visual C++5 through Visual C++ .NET 2003, you're compiling with /GX or /EHs, which enables the compiler's synchronousexception model. This model is defined to catch only exceptions resulting from aC++ throw statement, and there is no such statement in the above. If youwere to examine the assembly language emitted for this program, you would findthe compiler has optimized all the exception handling machinery out of thefunction, because the optimizer can determine the tried code cannot throw a C++exception. This is a great optimization! It's especially appreciated whenwriting template code. Unfortunately, there is a bug that causes catch(...)tocatch Win32 structured exceptions in some scenarios, which leads to the nextquestion.

Q2. I also wrote the code above, and I don't understand why the Win32 structured exception(SE) is caught in a debug build or when I compile with /EHa. Also, I sometimesfind that catch(...) catches SEs even in release builds when I use /GX. Isn'tcatch(...) supposed to catch only C++ exceptions?

A. According to Stroustrup, C++ exception handling (EH) is notintended to handle signals or other low-level, OS-specific events such asarithmetic exceptions. Win32 Structured Exceptions (SEs) clearly fall into thiscategory, and it should not be possible to catch SEs in catch(...). However, theC++ Standard doesn't specifically forbid this, and because anytimeyou raise an SE you invoke undefined behavior, it's "legal" for catch(...) to catch SEs, in a very technical sense, because the C++ Standardimposes no requirements on the behavior of a program that doessomething undefined, such as dereferencing a NULL pointer. That said, while itmay seem convenient to catch truly everything in catch(...), catching SEs thereis the source of numerous problems. Before discussing why I say that, let'sconsider how Visual C++ is documented to behave.

Visual C++ 5 and later define two EH models, called synchronous and asynchronous.The model chosen is determined by the /EH command line option. /EHs specifiesthe synchronous model, while /EHa specifies the asynchronous model. There is also /GX,which is defined by default for MFC and other AppWizard applications. /GX isequivalent to /EHsc, so it selects the synchronous model. (The cindicates that extern "C" functions do not throwexceptions.) The VC++ documentation defines the asynchronous model as follows:

In previous versions of Visual C++, the C++ exception handling mechanismsupported asynchronous (hardware) exceptions by default. Under theasynchronous model, the compiler assumes any instruction may generate anexception.

Under the asynchronous model, catch(...) catches SEs, and you must use /EHaif this is what you really want. You must also use /EHa if you're expecting tocatch SEs that have been translated into C++ exceptions with the help of _set_se_translator().(See Q4.)

The synchronous model is described as follows:

With the new synchronous exception model, now the default, exceptionscan be thrown only with a throw statement. Therefore, the compiler canassume that exceptions happen only at a throw statement or at afunction call. This model allows the compiler to eliminate the mechanics oftracking the lifetime of certain unwindable objects, and to significantlyreduce the code size, if the objects’ lifetimes do not overlap a functioncall or a throw statement.

The synchronous model is intended to provide C++ EH as Stroustrupintended,but unfortunately, in Visual C++ 5 through Visual C++ .NET 2003,  itdoesn't behave exactly as documented, and it's still possible to catchSEs in catch(...) if you compilewithout optimizations, or you compile with optimizations, and the optimizer isunable to determine the tried code cannot throw a C++ exception. For example, inVC5, if the tried code calls a function, the optimizer assumes it can throw,while in VC6, the function may need to live in another translation unit (sourcefile) to cause the optimizer to be pessimistic. Visual C++ .NET 2005 atlast corrects this problem for the synchronous model.

Q3. So what are the consequences of catching Win32 StructuredExceptions (SEs) in catch(...) ?

A. In order to answer this question, we first need to discuss what C++exceptions and SEs represent. According to Stroustrup, C++ exception handling iserror handling. For example, failure to acquire a resource such as memory orrunning out of disk space while writing to a file is an error that is often best reported by throwing an exception,especially when the resource is normally expected to be available. This greatlysimplifies code by eliminating the need to check function return codes, and ithelps you centralize error handling. This sort of error can occur in a correctlywritten program, and that is what C++ EH is intended to address.

On the other hand, SEs typically representprogram bugs. Everyone is familiar with access violations resulting fromdereferencing NULL pointers. The hardware detects this and traps,and Windows  turns the hardware event into an SE. In general, SEs representprogrammer errors, and correctly written programs have no such errors. SEs arealso used in the normal operation of the system. For example, it's possible touse VirtualAlloc() to reserve a region of your address space anddynamically commit pages as a program accesses uncommitted memory andcauses page faults. The program catches the SE in an __except clause,commits the memory, and resumes execution with the instruction that caused thefault. This should be invisible to C++ EH, which should not be able to interferewith it.

C++ exceptions and Win32 structured exceptions represent very differentthings. Problems caused by homogenizing them in catch(...) includethe following.

  1. If catch(...) is able to catch SEs, it's impossible to write the following with any confidence:

       // Begin exception-free code
       ... Update critical data structure
       // End exception-free code

    If the critical code has a bug that results in an SE, an outer catch(...) block may catch the SE, creating a completely unanticipated program state. The program may hobble along, further corrupting its state. If you're lucky, a subsequent uncaught SE will bring the program down before it does any serious damage, but debugging the problem may be much more difficult than if catch(...) hadn't swallowed the initial SE, because the secondary SE may occur in code far removed from the source of the actual bug. The OS will report the uncaught secondary SE and give you the opportunity to debug it, but it will lead you to the source of this SE, not the source of the actual problem.
  2. Code such as the following becomes suspect:

       try
       {
          TheFastButResourceHungryWay();
       }
       catch (...)
       {
          TheSlowButSureWay();
       }
       
    If a program bug or compiler code generation bug causes an access violation in the tried function, your discovery of the bug is hindered by catch(...) swallowing the SE. The only manifestation of the bug may be an inexplicable slowness, which may not be apparent in your testing, while if catch(...) hadn't caught the SE, you certainly would have discovered the bug while testing. (OS error boxes are pretty hard to miss!)
  3. The normal operation of the system is impaired. For example, the MFC CPropertySheet::DoModal() documentation describes a scenario in which you should not use catch(...). The exception raised by the DebugBreak API can be caught by catch(...), rendering DebugBreak useless. Also, if you're using __try/__except to handle SEs properly, you may have trouble if an interior catch(...) is present, even if it rethrows. You almost certainly will have trouble if your SE handler resumes execution with the faulting instruction. You may find the catch(...) block was entered and local variables destroyed, which is very bad if execution is resumed in its complementary try block. And if that try block subsequently throws a C++ exception, you may find yourself in an infinite loop with your SE filter function.
  4. Application frameworks are taking a chance if they guard your code with catch(...), which they normally should do. For example, MFC does not use catch(...), and as a result, an uncaught C++ exception terminates an MFC application.

Q4. What about _set_se_translator?

A. _set_se_translator is a function used to register another functionwhich translates Win32 structured exceptions  into true C++ exceptions. It allows you to partially avoid the catch(...) problemsdescribed in Q3 bywriting the following, where se_t is the type of object thrown by the translator function:

catch (se_t) { throw; }catch (...) { ... }

This is not a great workaround, because it's easy to forget to augment everycatch(...)as shown above, and you would have to establish a translator in every thread youcreate that runs code which uses this method, because SE handlers are attributesof a thread, and calling _set_se_translator in one thread has no effecton other threads. Also, the translator function isn't inherited by newthreads; thus, _set_se_translator has no effect on threads created afterit's called. Besides being difficult and error-prone to implement, thisworkaround can't account for code you didn't write and can't modify, and thiscan be an issue for library users.

Finally, the documentation does not make it clear that to use _set_se_translatorreliably, you must select the asynchronous EH model discussed inQ2, andthat tends to bloat your object code. If you don't do this, your code is subjectto the optimization discussed in Q1.

Q5. How do I deal with all this?

A. When using Visual C++ 5 through Visual C++ .NET 2003, the best course is to avoid catch(...) whenever possible.If you must use catch(...), be aware of all the issues described inthe preceding questions. If you're using Visual C++ .NET 2005, the /EHs optionbehaves as documented, the synchronous model works correctly, and you don't haveto worry about catch(...) catching SEs.

Q6. How do I safely use _com_error, std::exception, and other non-MFCexception classes in MFC programs?

A. MFC was designed before Visual C++ supported C++ exception handling. Theoriginal MFC implementation was based on macros such as TRY and CATCHand used setjmp and longjmp to simulate C++ exception handling. Tosimplify this initial implementation, MFC threw pointers to CExceptionobjects and pointers to objects of classes derived from CException, and CException*was the only exception type supported by early versions of MFC. ThoughMFC was updated to use C++ exceptions in Visual C++ 2.0, it was never made awareof other exception types, and the MFC source code continues to use the macros,which are now defined in terms of C++ EH. For example, MFC defines CATCH_ALL interms of: 

catch (CException* e)

Clearly, this doesn't catch all exceptions if the tried code usesthe C++ Standard Library, compiler COM support, or other libraries that definetheir own exception types. MFC does not itself use any exception type other thanCException*, but in many places, it wraps your code as follows:

TRY{// Call your code}CATCH_ALL(e){// Clean up and perhaps report the error to the user}END_CATCH_ALL

For example, an MFC WindowProc is guarded this way, because exceptions aren'tallowed to cross Windows message boundaries. However, CATCH_ALL catches onlyMFC exceptions, and if you fail to catch a non-MFC exception yourself, yourprogram will be terminated due to an uncaught exception. Even if you do catchthe exception yourself, where you catch it is still very important,because there are a number of functions within MFC that expect to catch allexceptions so they can clean up or return an error code to the caller through anormal function return statement. Now, if the try blocks within thesefunctions call into your code, and you don't translate non-MFC exceptions intoMFC exceptions right then and there, you allow non-MFC exceptions to propagatethrough MFC code that expects to catch everything, and as just described,it can't, and it doesn't. You may end up skipping some important clean-up code, and eventhough you catch your non-MFC exception at some outer level, it may be too late.This suggests the following rule of thumb:

Never allow a non-MFC exception to pass through MFC code

At a minimum, this means protecting every message handler that could exit viaa non-MFC exception with try/catch. Now, if a message handler can't doanything about an exception, and you want it to be reported to the user, it'soften appropriate for the handler to exit via an exception, because MFC willpresent the user with a nice message box describing the error, provided it cancatch it. To achieve this result, you need to translate non-MFC exceptions intoMFC exceptions. Macros can help here. For example, consider the code sketchedbelow:

class MfcGenericException : public CException{public:// CException overridesBOOL GetErrorMessage(LPTSTR lpszError,UINT nMaxError,PUINT pnHelpContext = 0){ASSERT(lpszError != 0);ASSERT(nMaxError != 0);if (pnHelpContext != 0)*pnHelpContext = 0;_tcsncpy(lpszError, m_msg, nMaxError-1);lpszError[nMaxError-1] = 0;return *lpszError != 0;}protected:explicit MfcGenericException(const CString& msg):  m_msg(msg){}private:CString m_msg;};class MfcStdException : public MfcGenericException{public:static MfcStdException* Create(const std::exception& ex){return new MfcStdException(ex);}private:explicit MfcStdException(const std::exception& ex): MfcGenericException(ex.what()){}};#define MFC_STD_EH_PROLOGUE try {#define MFC_STD_EH_EPILOGUE } catch (std::exception& ex) { throw MfcStdException::Create(ex); }

The code above defines a class, MfcGenericException, which is derivedfrom MFC's CException, and which serves as the base class for MfcStdExceptionand other non-MFC exception types. (We need this base class because MFC does notprovide a generic exception type that encapsulates a message string.) The macrosat the bottom are intended to surround your message handlers and other codecalled from MFC that can throw non-MFC exceptions. You use it like this:

void MyWnd::OnMyCommand(){MFC_STD_EH_PROLOGUE
   ... your code which can throw std::exception
MFC_STD_EH_EPILOGUE}

Together, the macros guard your code in a try block, and the MFC_STD_EH_EPILOGUEmacro translates std::exception into something MFC can catch, in thiscase, MfcStdException. Note that MfcStdException has a private constructor and defines a static Create function, and the latter providesthe only way to create an MfcStdException. It ensures the exceptionobject is created on the heap, which we must do, because each objectmaintains state information in the form of its error message. We can't simplythrow a pointer to a static instance, as AfxThrowMemoryException does,because that wouldn't be thread-safe due to our state information, and it's alsopossible to throw and catch an exception while handling another, which isultimately rethrown, and that would tend to overwrite the first message. Wecan't take any shortcuts here! Whoever catches our exception is responsible forcalling its Delete member function, inherited from CException.This function will delete the MfcStdException object, and it'sgood to disallow mistakes such as throwing a pointer to a local object bypreventing the creation of local objects altogether.

Using a technique such as the above is essential to creating MFCprograms which are robust in the presence of heterogeneous exception types. It's much easier thanwriting explicit try/catch blocks, and it allows exceptions to propagateto whoever can best handle them. In fact, explicit try/catch blocks arerelatively rare in well-designed programs, because the code is written in such away that the automatic stack unwinding and local variable destruction does theright thing. Consequently, the final step ofhandling an exception often amounts to simply letting the user know somethingwent wrong, and by translating your non-MFC exceptions into MFC exceptions, MFCcan handle that just fine.

Comments

To comment on this page, please send email todsh@mvps.org.