2009年9月9日星期三

A Visual C++ Exception FAQ

A Visual C++ Exception FAQ

Copyright © 2001-2007 Doug Harrison  (http://members.cox.net/doug_web/eh.htm)


This document answers some common questions concerning catch(...) and exceptions in general as implemented by Visual C++. It's structured mainly as a conversation, in which one question and answer leads to the next, so you'll get the most out of it if you read it as a whole. To give you a quick idea of what I'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 and beyond. The upcoming Visual C++ 2005 release, known also as "Whidbey", corrects one of the problems discussed below, and this partly affects questions Q1, Q2, and Q5, which are updated accordingly. The remaining questions and 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 with optimizations (e.g. /O1 or /O2).

#include <stdio.h>

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 synchronous exception model. This model is defined to catch only exceptions resulting from a C++ throw statement, and there is no such statement in the above. If you were to examine the assembly language emitted for this program, you would find the compiler has optimized all the exception handling machinery out of the function, because the optimizer can determine the tried code cannot throw a C++ exception. This is a great optimization! It's especially appreciated when writing template code. Unfortunately, there is a bug that causes catch(...) to catch Win32 structured exceptions in some scenarios, which leads to the next question.

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?

A. According to Stroustrup, C++ exception handling (EH) is not intended to handle signals or other low-level, OS-specific events such as arithmetic exceptions. Win32 Structured Exceptions (SEs) clearly fall into this category, and it should not be possible to catch SEs in catch(...). However, the C++ Standard doesn't specifically forbid this, and because anytime you raise an SE you invoke undefined behavior, it's "legal" for catch(...) to catch SEs, in a very technical sense, because the C++ Standard imposes no requirements on the behavior of a program that does something undefined, such as dereferencing a NULL pointer. That said, while it may seem convenient to catch truly everything in catch(...), catching SEs there is the source of numerous problems. Before discussing why I say that, let's consider 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 specifies the synchronous model, while /EHa specifies the asynchronous model. There is also /GX, which is defined by default for MFC and other AppWizard applications. /GX is equivalent to /EHsc, so it selects the synchronous model. (The c indicates that extern "C" functions do not throw exceptions.) The VC++ documentation defines the asynchronous model as follows:

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

Under the asynchronous model, catch(...) catches SEs, and you must use /EHa if this is what you really want. You must also use /EHa if you're expecting to catch 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, exceptions can be thrown only with a throw statement. Therefore, the compiler can assume that exceptions happen only at a throw statement or at a function call. This model allows the compiler to eliminate the mechanics of tracking the lifetime of certain unwindable objects, and to significantly reduce the code size, if the objects' lifetimes do not overlap a function call or a throw statement.

The synchronous model is intended to provide C++ EH as Stroustrup intended, but unfortunately, in Visual C++ 5 through Visual C++ .NET 2003,  it doesn't behave exactly as documented, and it's still possible to catch SEs in catch(...) if you compile without optimizations, or you compile with optimizations, and the optimizer is unable to determine the tried code cannot throw a C++ exception. For example, in VC5, 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 (source file) to cause the optimizer to be pessimistic. Visual C++ .NET 2005 at last corrects this problem for the synchronous model.

Q3. So what are the consequences of catching Win32 Structured Exceptions (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 is error handling. For example, failure to acquire a resource such as memory or running 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 greatly simplifies code by eliminating the need to check function return codes, and it helps you centralize error handling. This sort of error can occur in a correctly written program, and that is what C++ EH is intended to address.

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

C++ exceptions and Win32 structured exceptions represent very different things. Problems caused by homogenizing them in catch(...) include the 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 function which translates Win32 structured exceptions  into true C++ exceptions. It allows you to partially avoid the catch(...) problems described in Q3 by writing 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 every catch(...) as shown above, and you would have to establish a translator in every thread you create that runs code which uses this method, because SE handlers are attributes of a thread, and calling _set_se_translator in one thread has no effect on other threads. Also, the translator function isn't inherited by new threads; thus, _set_se_translator has no effect on threads created after it's called. Besides being difficult and error-prone to implement, this workaround can't account for code you didn't write and can't modify, and this can be an issue for library users.

Finally, the documentation does not make it clear that to use _set_se_translator reliably, you must select the asynchronous EH model discussed in Q2, and that tends to bloat your object code. If you don't do this, your code is subject to 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 in the preceding questions. If you're using Visual C++ .NET 2005, the /EHs option behaves as documented, the synchronous model works correctly, and you don't have to worry about catch(...) catching SEs.

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

A. MFC was designed before Visual C++ supported C++ exception handling. The original MFC implementation was based on macros such as TRY and CATCH and used setjmp and longjmp to simulate C++ exception handling. To simplify this initial implementation, MFC threw pointers to CException objects and pointers to objects of classes derived from CException, and CException* was the only exception type supported by early versions of MFC. Though MFC was updated to use C++ exceptions in Visual C++ 2.0, it was never made aware of 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 in terms of: 

catch (CException* e)

Clearly, this doesn't catch all exceptions if the tried code uses the C++ Standard Library, compiler COM support, or other libraries that define their own exception types. MFC does not itself use any exception type other than CException*, 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't allowed to cross Windows message boundaries. However, CATCH_ALL catches only MFC exceptions, and if you fail to catch a non-MFC exception yourself, your program will be terminated due to an uncaught exception. Even if you do catch the exception yourself, where you catch it is still very important, because there are a number of functions within MFC that expect to catch all exceptions so they can clean up or return an error code to the caller through a normal function return statement. Now, if the try blocks within these functions call into your code, and you don't translate non-MFC exceptions into MFC exceptions right then and there, you allow non-MFC exceptions to propagate through 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 even though 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 via a non-MFC exception with try/catch. Now, if a message handler can't do anything about an exception, and you want it to be reported to the user, it's often appropriate for the handler to exit via an exception, because MFC will present the user with a nice message box describing the error, provided it can catch it. To achieve this result, you need to translate non-MFC exceptions into MFC exceptions. Macros can help here. For example, consider the code sketched below:

class MfcGenericException : public CException
{
public:

// CException overrides
BOOL 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 derived from MFC's CException, and which serves as the base class for MfcStdException and other non-MFC exception types. (We need this base class because MFC does not provide a generic exception type that encapsulates a message string.) The macros at the bottom are intended to surround your message handlers and other code called 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_EPILOGUE macro translates std::exception into something MFC can catch, in this case, MfcStdException. Note that MfcStdException has a private constructor and defines a static Create function, and the latter provides the only way to create an MfcStdException. It ensures the exception object is created on the heap, which we must do, because each object maintains state information in the form of its error message. We can't simply throw a pointer to a static instance, as AfxThrowMemoryException does, because that wouldn't be thread-safe due to our state information, and it's also possible to throw and catch an exception while handling another, which is ultimately rethrown, and that would tend to overwrite the first message. We can't take any shortcuts here! Whoever catches our exception is responsible for calling its Delete member function, inherited from CException. This function will delete the MfcStdException object, and it's good to disallow mistakes such as throwing a pointer to a local object by preventing the creation of local objects altogether.

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

Comments

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

欢迎访问、交流!对本博客有何建议,请
来信告知!
本博内容来源于网络,如有不当或侵犯权益,请来信告知,将及时撤除!
如引用博客内容、论文,请注明原作者!

Google一下本博客