Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Exception safety is sort of like pregnancy...but hold that thought for a moment. We can't really talk reproduction until we've worked our way through courtship. 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:
Addressing the resource leak issue is easy, because Item 13 explains how to use objects to manage resources, and Item 14 introduces the Lock class as a way to ensure that mutexes are released in a timely fashion: void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); // from Item 14: acquire mutex and // ensure its later release delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); }
One of the best things about resource management classes like Lock is that they usually make functions shorter. See how the call to unlock is no longer needed? As a general rule, less code is better code, because there's less to go wrong and less to misunderstand when making changes. With the resource leak behind us, we can turn our attention to the issue of data structure corruption. Here we have a choice, but before we can choose, we have to confront the terminology that defines our choices. Exception-safe functions offer one of three guarantees:
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. Other than when dealing with exception-unsafe legacy code (which we'll discuss later in this Item), offering no exception safety guarantee should be an option only if your crack team of requirements analysts has identified a need for your application to leak resources and run with corrupt data structures. 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. First, we change the type of PrettyMenu's bgImage data member from a built-in Image* pointer to one of the smart resource-managing pointers described in Item 13. Frankly, this is a good idea purely on the basis of preventing resource leaks. The fact that it helps us offer the strong exception safety guarantee simply reinforces Item 13's argument that using objects (such as smart pointers) to manage resources is fundamental to good design. In the code below, I show use of TR1::shared_ptr, because its more intuitive behavior when copied generally makes it preferable to auto_ptr. Second, we reorder the statements in changeBackground so that we don't increment imageChanges until the image has been changed. As a general rule, it's a good policy not to change the status of an object to indicate that something has happened until something actually has. Here's the resulting code: 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; } Note that there's no longer a need to manually delete the old image, because that's handled internally by the smart pointer. Furthermore, the deletion takes place only if the new image is successfully created. More precisely, the tr1::shared_ptr::reset function will be called only if its parameter (the result of "new Image(imgSrc)") is successfully created. delete is used only inside the call to reset, so if the function is never entered, delete is never used. Note also that the use of an object (the TR1::shared_ptr) to manage a resource (the dynamically allocated Image) has again pared the length of changeBackground. 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. If the Image constructor throws an exception, it's possible that the read marker for the input stream has been moved, and such movement would be a change in state visible to the rest of the program. Until changeBackground addresses that issue, it offers only the basic exception safety guarantee. Let's set that aside, however, and pretend that changeBackground does offer the strong guarantee. (I'm confident you could come up with a way for it to do so, perhaps by changing its parameter type from an istream to the name of the file containing the image data.) 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 non-throwing operation. 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 std::tr1::shared_ptr<Image> bgImage; // Impl."; see below for int imageChanges; // why it's a struct }; 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 } // release the mutex
In this example, I've chosen to make PMImpl a struct instead of a class, because the encapsulation of PrettyMenu data is assured by pImpl being private. Making PMImpl a class would be at least as good, though somewhat less convenient. (It would also keep the object-oriented purists at bay.) If desired, PMImpl could be nested inside PrettyMenu, but packaging issues such as that are independent of writing exception-safe code, which is our concern here. 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-and-swap, 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 } It should be clear that if f1 or f2 is less than strongly exception-safe, it will be hard for someFunc to be strongly exception-safe. For example, suppose that f1 offers only the basic guarantee. For someFunc to offer the strong guarantee, it would have to write code to determine the state of the entire program prior to calling f1, catch all exceptions from f1, then restore the original state. Things aren't really any better if both f1 and f2 are strongly exception safe. After all, if f1 runs to completion, the state of the program may have changed in arbitrary ways, so if f2 then throws an exception, the state of the program is not the same as it was when someFunc was called, even though f2 didn't change anything. 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. Issues such as these can prevent you from offering the strong guarantee for a function, even though you'd like to. Another issue is efficiency. The crux of copy-and-swap is the idea of modifying a copy of an object's data, then swapping the modified data for the original in a non-throwing operation. This requires making a copy of each object to be modified, which takes time and space you may be unable or unwilling to make available. The strong guarantee is highly desirable, and you should offer it when it's practical, but it's not practical 100% of the time. When it's not, you'll have to offer the basic guarantee. In practice, you'll probably find that you can offer the strong guarantee for some functions, but the cost in efficiency or complexity will make it untenable for many others. As long as you've made a reasonable effort to offer the strong guarantee whenever it's practical, no one should be in a position to criticize you when you offer only the basic guarantee. For many functions, the basic guarantee is a perfectly reasonable choice. Things are different if you write a function offering no exception-safety guarantee at all, because in this respect it's reasonable to assume that you're guilty until proven innocent. You should be writing exception-safe code. But you may have a compelling defense. Consider again the implementation of someFunc that calls the functions f1 and f2. Suppose f2 offers no exception safety guarantee at all, not even the basic guarantee. That means that if f2 emits an exception, the program may have leaked resources inside f2. It means that f2 may have corrupted data structures, e.g., sorted arrays might not be sorted any longer, objects being transferred from one data structure to another might have been lost, etc. There's no way that someFunc can compensate for those problems. If the functions someFunc calls offer no exception-safety guarantees, someFunc itself can't offer any guarantees. Which brings me back to pregnancy. A female is either pregnant or she's not. It's not possible to be partially pregnant. Similarly, 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, because calls to that one function could lead to leaked resources and corrupted data structures. Unfortunately, much C++ legacy code was written without exception safety in mind, so many systems today are not exception-safe. They incorporate code that was written in an exception-unsafe manner. 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. Forty years ago, goto-laden code was considered perfectly good practice. Now we strive to write structured control flows. Twenty years ago, globally accessible data was considered perfectly good practice. Now we strive to encapsulate data. Ten years ago, writing functions without thinking about the impact of exceptions was considered perfectly good practice. Now we strive to write exception-safe code. Time goes on. We live. We learn. Things to Remember
|