Discussion
Team LiB
Previous Section Next Section

Discussion

These are key functions that must not fail because they are necessary for the two key operations in transactional programming: to back out work if problems are encountered during processing, and to commit work if no problems occur. If there's no way to safely back out using no-fail operations, then no-fail rollback is impossible to implement. If there's no way to safely commit state changes using a no-fail operation (notably, but not limited to, swap), then no-fail commit is impossible to implement.

Consider the following advice and requirements found in the C++ Standard:

If a destructor called during stack unwinding exits with an exception, terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor.

[C++03] §15.2(3)

No destructor operation defined in the C++ Standard Library [including the destructor of any type that is used to instantiate a standard library template] will throw an exception.

[C++03] §17.4.4.8(3)

Destructors are special, and the compiler invokes them automatically in various contexts. If you write a classlet's call it Nefariouswhose destructor might fail (usually by throwing an exception; see Item 72), you incur the following consequences:

  • Nefarious objects are hard to use safely in normal functions: You can't reliably instantiate automatic Nefarious objects in a scope if that scope might be exited through an exception. If that happened, Nefarious's destructor (automatically invoked) might attempt to throw an exception as well, which would result in sudden death of your entire program via std::terminate. (See also Item 75)

  • Classes with Nefarious members or bases are also hard to use safely: Nefarious' poor behavior extends to any class of which Nefarious is a member or a base class.

  • You can't reliably create global or static Nefarious objects either: Any exception its destructor might throw can't be caught.

  • You can't reliably create arrays of Nefarious: In short, the behavior of arrays is undefined in the presence of destructors that throw because there is no reasonable rollback behavior that could ever be devised. (Just think: What code can the compiler generate for constructing an array of ten Nefarious objects where, if the fourth object's constructor throws, the code has to give up and in its cleanup mode tries to call the destructors of the already-constructed objects… and one or more of those destructors throws? There is no satisfactory answer.)

  • You can't use Nefarious objects in standard containers: You can't store Nefarious objects in standard containers or use them with any other part of the standard library. The standard library forbids all destructors used with it from throwing.

Deallocation functions, including specifically overloaded operator delete and operator delete[], fall into the same category, because they too are used during cleanup in general, and during exception handling in particular, to back out of partial work that needs to be undone.

Besides destructors and deallocation functions, common error-safety techniques rely also on swap operations never failingin this case, not because they are used to implement a guaranteed rollback, but because they are used to implement a guaranteed commit. For example, here is an idiomatic implementation of operator= for a type T that performs copy construction followed by a call to a no-fail Swap:



T& T::operator=( const T& other ) {


 T temp( other );


 Swap( temp );


}



(See also Item 56.)

Fortunately, when releasing a resource, the scope for failure is definitely smaller. If using exceptions as the error reporting mechanism, make sure such functions handle all exceptions and other errors that their internal processing might generate. (For exceptions, simply wrap everything sensitive that your destructor does in a try/catch(…) block.) This is particularly important because a destructor might be called in a crisis situation, such as failure to allocate a system resource (e.g., memory, files, locks, ports, windows, or other system objects).

When using exceptions as your error handling mechanism, prefer documenting this behavior by declaring these functions with a commented empty exception specification of /* throw() */. (See Item 75.)

    Team LiB
    Previous Section Next Section