Discussion
Team LiB
Previous Section Next Section

Discussion

It's no coincidence that most modern languages created in the past 20 years use exceptions as their primary error reporting mechanism. Almost by definition, exceptions are for reporting exceptions to normal processingalso known as "errors," defined in Item 70 as violations of preconditions, postconditions, and invariants. Like all error reporting, exceptions should not arise during normal successful operation.

We will use the term "status codes" to cover all forms of reporting status via codes (including return codes, errno, a GetLastError function, and other strategies to return or retrieve codes), and "error codes" specifically for status codes that signify errors. In C++, reporting errors via exceptions has clear advantages over reporting them via error codes, all of which make your code more robust:

  • Exceptions can't be silently ignored: The most terrible weakness of error codes is that they are ignored by default; to pay the slightest attention to an error code, you have to explicitly write code to accept the error and respond to it. It is common for programmers to accidentally (or lazily) fail to pay attention to error codes. This makes code reviews harder. Exceptions can't be silently ignored; to ignore an exception, you must explicitly catch it (even if only with catch(…)) and choose not to act on it.

  • Exceptions propagate automatically: Error codes are not propagated across scopes by default; to inform a higher-level calling function of a lower-level error code, a programmer writing the intervening code has to explicitly hand-write code to propagate the error. Exceptions propagate across scopes automatically exactly until they are handled. ("It is not a good idea to try to make every function a fire-wall." [Stroustrup94, §16.8])

  • Exception handling removes error handling and recovery from the main line of control flow: Error code detection and handling, when it is written at all, is necessarily interspersed with (and therefore obfuscates) the main line of control flow. This makes both the main control flow and the error handling code harder to understand and maintain. Exception handling naturally moves error detection and recovery into distinct catch blocks; that is, it makes error handling distinctly modular instead of inline spaghetti. This makes the main line of control more understandable and maintainable, and it's more than just of aesthetic benefit to distinguish clearly between correct operation and error detection and recovery.

  • Exception handling is better than the alternatives for reporting errors from constructors and operators: The copy constructors and the operators have predefined signatures that leave no room for return codes. In particular, constructors have no return type at all (not even void), and for example every operator+ must take exactly two parameters and return one object (of a prescribed type; see Item 26). For operators, using error codes is at least possible if not desirable; it would require errno-like approaches, or inferior solutions like packaging status with an object. For constructors, using error codes is not feasible because the C++ language tightly binds together constructor exceptions and constructor failures so that the two have to be synonymous; if instead we used an errno-like approach such as

    
    
    SomeType anObject;                      // construct an object
    
    
    if( SomeType::ConstructionWasOk() ) {   // test whether construction worked
    
    
      // …
    
    
    

    then not only is the result ugly and error-prone, but it leads to misbegotten objects that don't really satisfy their type's invariantsnever mind the race conditions inherent in calls to SomeType::ConstructionWasOk in multithreaded applications. (See [Stroustrup00] §E.3.5.)

The main potential drawback of exception handling is that it requires programmers to be familiar with a few recurring idioms that arise from exceptions' out-of-band control flow. For example, destructors and deallocation functions must not fail (see Item 51), and intervening code must be correct in the face of exceptions (see Item 71 and References); to achieve the latter, a common coding idiom is to perform all the work that might emit an exception safely off to the side and only then, when you know that the real work has succeeded, you commit and modify the program state using only operations that provide the no-fail guarantee (see Item 51 and [Sutter00] §9-10, §13). But then using error codes also has its own idioms; those have just been around longer and so more people already know thembut, unfortunately, also commonly and routinely ignore them. Caveat emptor.

Performance is not normally a drawback of exception handling. First, note that you should turn always on exception handling in your compiler even if it is off by default, otherwise you won't get standard behavior and error reporting from the C++ language operations such as operator new and standard library operations such as STL container insertions (see Exceptions).

[Aside: Turning on exception handling can be implemented so that it increases the size of your executable image (this part is unavoidable) but incurs zero or near-zero performance overhead in the case where no exception is thrown, and some compilers do so. Other compilers do incur overhead, especially when providing secure modes to prevent malicious code from attacking the exception handling mechanism via buffer overruns. Whether there is overhead or not, at minimum do always at least turn on your compiler's support for exception handling, because otherwise the language and standard library won't report errors correctly. We mention this because we know of projects that turn off exception handling, which is a far-reaching fundamental decision and should never be done except as a last resort and with extreme care (see Exceptions).]

Once your compiler's support for exception handling is turned on, the performance difference between throwing an exception and returning an error code is typically negligible in normal processing (the case where no error occurs). You may notice a performance difference in the case where an error does occur, but if you're throwing so frequently that the exception throwing/catching handling performance overhead is actually noticeable, you're almost certainly using exceptions for conditions that are not true errors and therefore not correctly distinguishing between errors and non-errors (see Item 70). If they're really errors that truly violate pre- and postconditions and invariants, and if they're really happening that frequently, the application has far more serious problems.

One symptom of error code overuse is when application code needs to check relentlessly for trivially true conditions, or (worse) fails to check error codes that it should check.

One symptom of exception overuse is when application code throws and catches exceptions so frequently that a TRy block's success count and failure count are within the same order of magnitude. Such a catch block is either not really handling a true error (one that violates preconditions, postconditions, or invariants), or the program is in serious trouble.

    Team LiB
    Previous Section Next Section