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:
class PrettyMenu { public: // ... void changeBackground(std::istream& imgSrc); // change background image // ... private: Mutex mutex; // mutex for this object Image *bgImage; // current background image int imageChanges; // # of times image has been changed };
Consider this possible implementation of PrettyMenu‘s changeBackground function:
void PrettyMenu::changeBackground(std::istream& imgSrc) { lock(&mutex); // acquire mutex (as in Item 14) 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. (On the other hand, the old image has definitely been eliminated, so I suppose you could argue that the image has been "changed.")
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.
Exception-safe code must offer one of the three guarantees above. If it doesn’t, it’s not exception-safe. The choice, then, is to determine which guarantee to offer for each of the functions you write.
As a general rule, you want to offer the strongest guarantee that’s practical. From an exception safety point of view, nothrow functions are wonderful, but it’s hard to climb out of the C part of C++ without calling functions that might throw. Anything using dynamically allocated memory (e.g., all STL containers) typically throws a bad_alloc exception if it can’t find enough memory to satisfy a request (see Item 49). Offer the nothrow guarantee when you can, but for most functions, the choice is between the basic and strong guarantees.
In the case of changeBackground, almost offering the strong guarantee is not difficult.
class PrettyMenu { // ... std::tr1::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; }
As I said, those two changes almost suffice to allow changeBackground to offer the strong exception safety guarantee. What's the fly in the ointment? The parameter imgSrc.
There is a general design strategy that typically leads to the strong guarantee, and it’s important to be familiar with it. The strategy is known as "copy and swap." In principle, it’s very simple. Make a copy of the object you want to modify, then make all needed changes to the copy. If any of the modifying operations throws an exception, the original object remains unchanged. After all the changes have been successfully completed, swap the modified object with the original in a nonthrowing operation (see Item 25).
This 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. This is often known as the "pimpl idiom," and Item 31 describes it in some detail. For PrettyMenu, it would typically look something like this:
struct PMImpl { // PMImpl = "PrettyMenu Impl." see below for why it's a struct std::tr1::shared_ptr<Image> bgImage; int imageChanges; }; class PrettyMenu { // ... private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; // see Item 25 Lock ml(&mutex); // acquire the mutex std::tr1::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 }
The copy-and-swap strategy is an excellent way to make all-or-nothing changes to an object’s state, but, in general, it doesn’t guarantee that the overall function is strongly exception-safe.
To see why, consider an abstraction of changeBackground, someFunc, that uses copy-andswap, but that includes calls to two other functions, f1 and f2:
void someFunc() { // ... // make copy of local state f1(); f2(); // ... // swap modified state into place }
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. If a side effect of calling f1, for example, is that a database is modified, it will be hard to make someFunc strongly exception-safe. There is, in general, no way to undo a database modification that has already been committed; other database clients may have already seen the new state of the database.
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. (Again, see Item 13.) That will prevent resource leaks. Follow that by determining which of the three exception safety guarantees is the strongest you can practically offer for each function you write, settling for no guarantee only if calls to legacy code leave you no choice. Document your decisions, both for clients of your functions and for future maintainers. A function's exception-safety guarantee is a visible part of its interface, so you should choose it as deliberately as you choose all other aspects of a function's interface.
Things to Remember:
- 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, or nothrow guarantees.
- The strong guarantee can often be implemented via copy-and-swap, but the strong guarantee is not practical for all functions.
- A function can usually offer a guarantee no stronger than the weakest guarantee of the functions it calls.