Discussion
Team LiB
Previous Section Next Section

Discussion

The basic, strong, and no-fail (then known as nothrow) guarantees were originally described in [Abrahams96] and publicized in [GotW], [Stroustrup00] §E.2, and [Sutter00] with respect to exception safety. They apply to all error handling regardless of the specific method used, and so we will use them to describe error handling safety in general. The no-fail guarantee is a strict superset of the strong guarantee, and the strong guarantee is a strict superset of the basic guarantee.

In general, every function should provide the strongest guarantee that it can provide without needlessly penalizing calling code that doesn't need the guarantee. Where possible, it should additionally provide enough functionality to allow calling code that needs a still stronger guarantee to achieve that (see the vector::insert case in the Examples).

Ideally, we write functions that always succeed and therefore can provide the no-fail guarantee. Certain functions must always provide the no-fail guarantee, notably destructors, deallocation functions, and swap functions (see Item 51).

Most functions, however, can fail. When errors are possible, the safest approach is to ensure that a function supports a transactional behavior: Either it totally succeeds and takes the program from the original valid state to the desired target valid state, or it fails and leaves the program in the state it was before the callany object's visible state before the failed call is the same after the failed call (e.g., a global int's value won't be changed from 42 to 43) and any action that the calling code would have been able to take before the failed call is still possible with the same meaning after the failed call (e.g., no iterators into containers have been invalidated, performing ++ on the aforementioned global int will yield 43 not 44). This is the strong guarantee.

Finally, if providing the strong guarantee is difficult or needlessly expensive, provide the basic guarantee: Either the function totally succeeds and reaches the intended target state, or it does not completely succeed and leaves the program in a state that is valid (preserves the invariants that the function knows about and is responsible for preserving) but not predictable (it might or might not be the original state, and none, some, or all of the postconditions could be met; but note that all invariants must still be reestablished). The design of your application must prepare for handling that state appropriately.

That's it; there is no lower level. A failure to meet at least the basic guarantee is always a program bug. Correct programs meet at least the basic guarantee for all functions; even those few correct programs that deliberately leak resources by design, particularly in situations where the program immediately aborts, do so knowing that they will be reclaimed by the operating system. Always structure code so that resources are correctly freed and data is in a consistent state even in the presence of errors, unless the error is so severe that graceful or ungraceful termination is the only option.

When deciding which guarantee to support, consider also versioning: It's always easy to strengthen the guarantee in a later release, whereas loosening a guarantee later will break calling code that has come to rely on the stronger guarantee.

Remember that "error-unsafe" and "poor design" go hand in hand: If it is difficult to make a piece of code satisfy even the basic guarantee, that almost always is a signal of its poor design. For example, a function having multiple unrelated responsibilities is difficult to make error-safe (see Item 5).

Beware if a copy assignment operator relies on a check for self-assignment in order to function correctly. An error-safe copy assignment operator is automatically safe for self-assignment. It's all right use a self-assignment check as an optimization to avoid needless work. (See Item 55.)

    Team LiB
    Previous Section Next Section