Discussion
Team LiB
Previous Section Next Section

Discussion

The C++ Standard does not specify the way exception propagation has to be implemented, and there isn't even a de facto standard respected by most systems. The mechanics of exception propagation vary not only with the operating system and compiler used, but also with the compiler options used to build each module of your application with a given compiler on a given operating system. Therefore, an application must prevent exception handling incompatibilities by shielding the boundaries of each of its major modules, meaning each unit for which the developer can ensure that the same compiler and options are consistently used.

At a minimum, your application must have catch-all catch(…) seals in the following places, most of which apply directly to modules:

  • Around main: Catch and log any otherwise uncaught exception that's about to end your program ignominiously.

  • Around callbacks from code you don't control: Operating systems and libraries offer frameworks in which you pass a pointer to a function to be invoked later (e.g., when an asynchronous event occurs). Don't allow exceptions to propagate out of your callback, because it's very possible that the code invoking your callback does not use the same exception handling mechanism. For that matter, it might not even have been written in C++.

  • Around thread boundaries: Ultimately, a thread is created from within the bowels of the operating system. Make sure your thread mainline function doesn't surprise the system by passing it an exception.

  • Around module interface boundaries: Your subsystem will expose some public interface for the rest of the world to use. If the subsystem is packaged as a separate library, prefer to confine exceptions inside and use ye olde staid-but-stable error codes for signaling errors to the outer world. (See Item 72.)

  • Inside destructors: Destructors don't throw (see Item 51). Destructors that call functions that might throw exceptions need to protect themselves against leaking those exceptions.

Ensure that each module consistently uses a single error handling strategy internally (preferably C++ exceptions; see Item 72) and a single error handling strategy in its interface (e.g., error codes for a C API); the two might happen to be the same, but they usually aren't. Error handling strategies change only on module boundaries. Make clear how to interface the strategies between modules (e.g., how to interact with COM or CORBA, or to always catch exceptions at a C API boundary). A good solution is to define central functions that translate between exceptions and error codes returned by a subsystem. This way, you can easily translate incoming errors from peer modules into your internally used exceptions and ease integration.

Using two strategies instead of one may sound like overkill and you might be tempted to forgo exceptions and only use the good old error codes throughout. But don't forget that exception handling has real ease-of-use and robustness advantages, is idiomatic C++, and can't be avoided in nontrivial C++ programs (because the standard language and library throw exceptions), so you should prefer to use exception handling when you can. For more details, see Item 72.

A word of caution: Some operating systems use the C++ exception mechanism to piggyback low-level, system-specific errors, such as dereferencing a null pointer. Consequently, a catch(…) clause can catch more things than just straight C++ exceptions, and so your program might be in a twilight zone by the time the catch(…) is executing. Consult your system's documentation and either be prepared for handling such low-level exceptions in the smartest way you can devise, or use system-specific calls to disable such piggybacking at the start of your application. Replacing catch(…) with a series of catch(E1&) {/*…*/ }catch(E2&) {/*…*/ }… catch(En&) {/*…*/ } for each known base exception type Ei is not a scalable solution because you'd need to update the caught list whenever you add a new library (using its own exception hierarchy) to your application.

Using catch(…) in places other than those listed in this Item is often a sign of bad design, because it means you are eager to catch absolutely all exceptions without necessarily having specific knowledge about how to handle them (see Item 74). A good program doesn't have many catch-alls and, indeed, not many try/catch statements at all; ideally, errors are propagated smoothly throughout, translated across module boundaries (a necessary evil), and handled at strategically placed boundaries.

    Team LiB
    Previous Section Next Section