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.,
int
s, pointers, etc.) are nothrow. This is a critical building block of exception-safe code.
Example
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.
Resource leak
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
bgImage
data member inPrettyMunu
from a built-inImage*
pointer to smart pointer such astr1::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
imageChanges
until the image has been changed.
|
|
|
|
Note how the use of resource magangement object (i.e., the smart pointer here) helps:
- The
tr1::shared_ptr::reset
function will be called only if its parameter (the result ofnew Image(imgSrc)
) is successfully created - The
delete
operation for the old image is inside thereset
, so if thereset
function 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
delete
the old image, and the length ofchangeBackground
reduces
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.
Copy-and-swap
strategy
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
Suppose someFunc
uses copy-and-swap
and includes calls to two other functions, f1
and f2
:
|
|
Apparently, if f1
or 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 f1
and 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 f1
, if 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.
2. Efficiency
Copy and 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.
In practice
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.