Suppose we have a class for representing GUI menus with background images. The class is designed to be used in a threaded environment, so it has a mutex for concurrency control.
1 | class PrettyMenu { |
From the perspective of exception safety, this function is about as bad as it gets. There are two requirements for exception safety, and this satisfies neither.
When an exception is thrown, exception-safe functions:
- Leak no resources. The code above fails this test, because if the “new Image(imgSrc)” expression yields an exception, the call to unlock never gets executed, and the mutex is held forever.
- Don’t allow data structures to become corrupted. If “new Image(imgSrc)” throws,
bgImage
is left pointing to a deleted object. In addition,imageChanges
has been incremented, even though it’s not true that a new image has been installed.
Exception-safe functions offer one of three guarantees:
- Functions offering the basic guarantee promise that if an exception is thrown, everything in the program remains in a valid state. No objects or data structures become corrupted, and all objects are in an internally consistent state (e.g., all class invariants are satisfied). However, the exact state of the program may not be predictable. For example, we could write changeBackground so that if an exception were thrown, the PrettyMenu object might continue to have the old background image, or it might have some default background image, but clients wouldn’t be able to predict which. (To find out, they’d presumably have to call some member function that would tell them what the current background image was.)
- Functions offering the strong guarantee promise 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 succeed completely, and if they fail, the program state is as if they’d never been called.
- Functions offering the nothrow guarantee promise never to throw exceptions, because they always do what they promise to do. All operations on built-in types (e.g., ints, pointers, etc.) are nothrow (i.e., offer the nothrow guarantee). This is a critical building block of exception-safe code.
Working with functions offering the strong guarantee is easier than working with functions offering only the basic guarantee, because after calling a function offering the strong guarantee, there are only two possible program states: as expected following successful execution of the function, or the state that existed at the time the function was called. In contrast, if a call to a function offering only the basic guarantee yields an exception, the program could be in any valid state.
If we write changeBackground
as following
1 | class PrettyMenu { |
changeBackground
now offers strong exception-safe guarantee assuming new Image(imgSrc)
won’t modify any state of imgSrc
.
A very common strategy to provide strong exception-safe guarantee is to use copy-and-swap
strategy. It’s extremely helpful when you have many state need to be managed or restored when exception is thrown. copy-and-swap
strategy goes very well with pimpl idiom
.
1 | struct PMImpl { // PMImpl = "PrettyMenu |
As you can see, it doesn’t matter how many object has changed, as long as we don’t do the last swap
step, objects will remain in their original state. Of course copy-and-swap
comes with a cost of copying the object.
Another very important point is that even the function you are calling provides strong exception-safe guarantee, it doesn’t mean your function automatically can provide exception-safe guarantee. For example
1 | void someFunc() |
If both f1
and f2
provide strong exception-safe guarantee, and f1
executed without any exception and modified some database value, and then f2
throws exception. How can someFunc
restore database modification to it’s original value? This is really tough or almost impossible to achieve.
The problem is side effects. As long as functions operate only on local state (e.g., someFunc
affects only the state of the object on which it’s invoked), it’s relatively easy to offer the strong guarantee. When functions have side effects on non-local data, it’s much harder.