Exception-safe functions leak no resources and allow no data structures to become corrupted, even when exceptions are thrown. Such functions offer the basic, strong, and nothrow guarantees.
For exception-safe functions, there are two requirements when an exception is thrown:
- Leak no resources
- Don’t allow data structures to become corrupted
Specifically, from the perspective of data structure corruption, exception-safe functions must offer one of three guarantees below from the weakest to the strongest:
- The basic guarantee promises that if an exception is thrown, everything in the program remains in a valid state - all class invariants are satisfied, but the exact state of the program may not be predictable.
- The strong guarantee promises that if an exception is thrown, the state of the program is unchanged - calls to such functions are atomic in the sense that if they succeed, they successd completely, and if they fail, the program state is as if they’d never been called.
- The nothrow guarantee promises never to throw exceptions - all operators on built-in types (e.g.,
ints, pointers, etc.) are nothrow. This is a critical building block of exception-safe code.
With all these terminologies bear in mind, let’s see an example representing exception-unsafe style. Suppose there’s a class for GUI menus with background images, and it will be used in a threaded environment, so it has a mutex for concurrency control:
Firstly, the code above is likely to encounter resource leak, because if the
new Image(imgSrc) expression yields an exception, the call to
unlock never gets executed, and the mutex is held forever.
Secondly, this function guarantees none of the 3 promises in terms of data structure corruption above: when
new Image(Src) throws,
bgImage is left pointing to a deleted object, and
imageChanges has been increamented before the new image has been installed, resulting to invalid object state.
To address the resource leak issue, we can use objects to manage resources (item 13), and take advantage of
Lock class to ensure that mutexes are released in a timely fashion (item 14):
Data structure corruption
To address the issue of data structure corruption, we may need to determine which guarantee to offer. As a general rule,
we want to offer the strongest guarantee that’s practical.
Note the word practical. We definitely want to offer nothrow guarantee for every functions we write, but it’s hard to keep such a promise - to name a common exception: anything using dynamically allocated memory (e.g., all STL containers) runs the risk of a
bad_alloc exception if it can’t find enough memory to satisfy a request (item 49). For most functions, the choice for us is between the basic and strong guarantees.
In the case of
changeBackground, almost offering the strong guarantee is not difficult:
- firstly, we change the type of
bgImagedata member in
PrettyMunufrom a built-in
Image*pointer to smart pointer such as
tr1::shraed_ptr(item 13), which benefits us with
- preventing resource leaks
- offering strong exception safety guarantee
- secondly, we reorder the statements so that we don’t increment
imageChangesuntil the image has been changed.
Note how the use of resource magangement object (i.e., the smart pointer here) helps:
tr1::shared_ptr::resetfunction will be called only if its parameter (the result of
new Image(imgSrc)) is successfully created
deleteoperation for the old image is inside the
reset, so if the
resetfunction is never entered (the program somehow fails to create new image), the deletion of the old image will never take place
- As a result, the deletion takes place only if the new image is successfully created
- We don’t need to manually
deletethe old image, and the length of
After these two changes,
changeBackground almost offer the strong exception safety guarantee. The only weakness now is the parameter
imgSrc: if the
Image constructor throws an exception, it’s possible that the read marker for the input stream has been moved, which is a change in state visible to the rest of the program, leading to offering only the basic exception safety guarantee.
There actually is a general design strategy for offering the strong guarantee:
copy and swap strategy:
Make a copy of the object we want to modify, then make all needed changes to the copy;
- If all the changes have been successfully completed, swap the modified object with the original in a non-throwing operation (item 25);
- If any of the modifying operations throws an exception, the original object remains unchanged.
The strategy is usually implemented by putting all the per-object data from the “real” object into a separate implementation object, then giving the real object a pointer to its implementation object (know as the “pimpl idiom”, item 31). For
PrettyMenu, it would look something like this:
We don’t have to make the struct
PMImpl as a class, because the encapsulation of
PrettyMenu data is assured by
pImpl being private, and it is more convenient to use struct. If desired,
PMImpl could be nested inside
PrettyMenu when considering packaging issues.
Side effects and efficiency
Even with the help of copy-and-
swap strategy, there are two possible reasons that downgrade the overall exception safety level from strong to basic: side effects and efficiency.
1. Side effects
someFunc uses copy-and-
swap and includes calls to two other functions,
f2 is less than strongly exception-safe, it will be hard for
someFunc to be strong exception-safe. For example, suppose
f1 offers only the basic guarantee, in order to offer the strong guarantee for
someFunc, we have to write code to determine the state of the entire program before calling
f1, catch all exceptions from
f1, and then store the original state. It’s complicated, but it’s doable. However, even if
f2 are both strongly exception safe, as long as there are side effects on non-local data, it’s much harder to offer the strong guarantee.
For example, if a side effect of calling
f1 is that a database is modified, and there is, in general, no way to undo a database modification that has already been committed; so after successfully calling
f2 then throws an exception, the state of the program is not the same as it was when calling
someFunc, even though
f2 didn’t change anything.
swap strategy requires making a copy of each object to be modified, which takes time and space we may be unable or unwilling to make available. It’s just not practical 100% of the time when we want to offer the strong guarantee.
When it’s not, we’ll have to offer the basic guarantee. In practice, we can usually offer the strong guarantee for some functions, but the const in efficiency or complexity will make it untenable for many others. For those functions, the basic guarantee is a perfectly resonable choice, as long as we’ve made a reasonable effort to offer the strong guarantee whenever it’s practical.
A software system is either exception-safe or it’s not. There’s no such thing as a partially exception-safe system. If a system has even a single function that’s not exception-safe, the system as a whole is not exception-safe. Unfortunately, much C++ legacy code was written without exception safety in mind, so many system incorporating legacy code today are not exception-safe.
There’s no reason to perpetuate this state of affairs. When writing new code or modifying existing code, think carefully about how to make it exception-safe:
- begin by using objects to manage resources to prevent resource leaks
- follow by determining which of the three exception safety guarantees is the strongest we cound practically offer for each function, settling for no guarantee only if calls to legacy code leave us no choice.
- Document our decisions, both for clients of our functions and for future maintainers - a function’s exception-safety guaranteee is a visible part of its interface, so we should choose it as deliberately as we choose all other aspects of a function’s interface.