Effective C++ item 29: Strive For Exception-safe Code

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc);
...
private:
Mutex mutex;
Image *bgImage;
int imageChanges;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage; // get rid of old background
++imageChanges; // update image change count
bgImage = new Image(imgSrc); // install new background
unlock(&mutex); // release mutex
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PrettyMenu {
...
std::shared_ptr<Image> bgImage;
...
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);

bgImage.reset(new Image(imgSrc)); // replace bgImage's internal
// pointer with the result of the
// "new Image" expression
++imageChanges;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct PMImpl {                               // PMImpl = "PrettyMenu
std::shared_ptr<Image> bgImage; // Impl."; see below for
int imageChanges; // why it's a struct
};
class PrettyMenu {
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
Lock ml(&mutex);
std::shared_ptr<PMImpl> // copy obj. data
pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); // modify the copy
++pNew->imageChanges;
swap(pImpl, pNew); // swap the new
// data into place
}

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
2
3
4
5
6
7
void someFunc()
{
... // make copy of local state
f1();
f2();
... // swap modified state into place
}

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.

Reference:
“Effective C++” Third Edition by Scott Meyers.