Item29: Strive for exception-safe code.


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:

  • 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.")

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:

  • 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.

    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.

  • 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.

    It might seem reasonable to assume that functions with an empty exception specification are nothrow, but this isn't necessarily true. For example, consider this function:

     int doSomething() throw();          // note empty exception spec. 

    This doesn't say that doSomething will never throw an exception; it says that if doSomething tHRows an exception, it's a serious error, and the unexpected function should be called.[1] In fact, doSomething may not offer any exception guarantee at all. The declaration of a function (including its exception specification, if it has one) doesn't tell you whether a function is correct or portable or efficient, and it doesn't tell you which, if any, exception safety guarantee it offers, either. All those characteristics are determined by the function's implementation, not its declaration.

    [1] For information on the unexpected function, consult your favorite search engine or comprehensive C++ text. (You'll probably have better luck searching for set_unexpected, the function that specifies the unexpected function.)

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

  • 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.




Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
ISBN: 321334876
EAN: N/A
Year: 2006
Pages: 102

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net