Item 13: Use objects to manage resources.

Suppose we're working with a library for modeling investments (e.g., stocks, bonds, etc.), where the various investment types inherit from a root class Investment:

 class Investment { ... };            // root class of hierarchy of                                      // investment types 

Further suppose that the way the library provides us with specific Investment objects is through a factory function (see Item 7):

 Investment* createInvestment();  // return ptr to dynamically allocated                                  // object in the Investment hierarchy;                                  // the caller must delete it                                  // (parameters omitted for simplicity) 

As the comment indicates, callers of createInvestment are responsible for deleting the object that function returns when they are done with it. Consider, then, a function f written to fulfill this obligation:

 void f() {   Investment *pInv = createInvestment();         // call factory function   ...                                            // use pInv   delete pInv;                                   // release object } 

This looks okay, but there are several ways f could fail to delete the investment object it gets from createInvestment. There might be a premature return statement somewhere inside the "..." part of the function. If such a return were executed, control would never reach the delete statement. A similar situation would arise if the uses of createInvestment and delete were in a loop, and the loop was prematurely exited by a continue or goto statement. Finally, some statement inside the "..." might throw an exception. If so, control would again not get to the delete. Regardless of how the delete were skipped, we'd leak not only the memory containing the investment object but also any resources held by that object.

Of course, careful programming could prevent these kinds of errors, but think about how the code might change over time. As the software gets maintained, somebody might add a return or continue statement without fully grasping the repercussions on the rest of the function's resource management strategy. Even worse, the "..." part of f might call a function that never used to throw an exception but suddenly starts doing so after it has been "improved." Relying on f always getting to its delete statement simply isn't viable.

To make sure that the resource returned by createInvestment is always released, we need to put that resource inside an object whose destructor will automatically release the resource when control leaves f. In fact, that's half the idea behind this Item: by putting resources inside objects, we can rely on C++'s automatic destructor invocation to make sure that the resources are released. (We'll discuss the other half of the idea in a moment.)

Many resources are dynamically allocated on the heap, are used only within a single block or function, and should be released when control leaves that block or function. The standard library's auto_ptr is tailor-made for this kind of situation. auto_ptr is a pointer-like object (a smart pointer) whose destructor automatically calls delete on what it points to. Here's how to use auto_ptr to prevent f's potential resource leak:

 void f() {   std::auto_ptr<Investment> pInv(createInvestment());  // call factory                                                        // function   ...                                                  // use pInv as                                                        // before }                                                      // automatically                                                        // delete pInv via                                                        // auto_ptr's dtor 

This simple example demonstrates the two critical aspects of using objects to manage resources:

  • Resources are acquired and immediately turned over to resource-managing objects. Above, the resource returned by createInvestment is used to initialize the auto_ptr that will manage it. In fact, the idea of using objects to manage resources is often called Resource Acquisition Is Initialization (RAII), because it's so common to acquire a resource and initialize a resource-managing object in the same statement. Sometimes acquired resources are assigned to resource-managing objects instead of initializing them, but either way, every resource is immediately turned over to a resource-managing object at the time the resource is acquired.

  • Resource-managing objects use their destructors to ensure that resources are released. Because destructors are called automatically when an object is destroyed (e.g., when an object goes out of scope), resources are correctly released, regardless of how control leaves a block. Things can get tricky when the act of releasing resources can lead to exceptions being thrown, but that's a matter addressed by Item 8, so we'll not worry about it here.

Because an auto_ptr automatically deletes what it points to when the auto_ptr is destroyed, it's important that there never be more than one auto_ptr pointing to an object. If there were, the object would be deleted more than once, and that would put your program on the fast track to undefined behavior. To prevent such problems, auto_ptrs have an unusual characteristic: copying them (via copy constructor or copy assignment operator) sets them to null, and the copying pointer assumes sole ownership of the resource!

 std::auto_ptr<Investment>                 // pInv1 points to the   pInv1(createInvestment());              // object returned from                                           // createInvestment std::auto_ptr<Investment> pInv2(pInv1);   // pInv2 now points to the                                           // object; pInv1 is now null pInv1 = pInv2;                            // now pInv1 points to the                                           // object, and pInv2 is null 

This odd copying behavior, plus the underlying requirement that resources managed by auto_ptrs must never have more than one auto_ptr pointing to them, means that auto_ptrs aren't the best way to manage all dynamically allocated resources. For example, STL containers require that their contents exhibit "normal" copying behavior, so containers of auto_ptr aren't allowed.

An alternative to auto_ptr is a reference-counting smart pointer (RCSP). An RCSP is a smart pointer that keeps track of how many objects point to a particular resource and automatically deletes the resource when nobody is pointing to it any longer. As such, RCSPs offer behavior that is similar to that of garbage collection. Unlike garbage collection, however, RCSPs can't break cycles of references (e.g., two otherwise unused objects that point to one another).

TR1's tr1::shared_ptr (see Item 54) is an RCSP, so you could write f this way:

 void f() {   ...   std::tr1::shared_ptr<Investment>     pInv(createInvestment());             // call factory function   ...                                     // use pInv as before }                                         // automatically delete                                           // pInv via shared_ptr's dtor 

This code looks almost the same as that employing auto_ptr, but copying shared_ptrs behaves much more naturally:

 void f() {   ...   std::tr1::shared_ptr<Investment>          // pInv1 points to the     pInv1(createInvestment());              // object returned from                                             // createInvestment   std::tr1::shared_ptr<Investment>          // both  pInv1 and pInv2 now     pInv2(pInv1);                           // point to the object   pInv1 = pInv2;                            // ditto   nothing has                                             // changed   ... }                                           // pInv1 and pInv2 are                                             // destroyed, and the                                             // object they point to is                                             // automatically deleted 

Because copying tr1::shared_ptrs works "as expected," they can be used in STL containers and other contexts where auto_ptr's unorthodox copying behavior is inappropriate.

Don't be misled, though. This Item isn't about auto_ptr, tr1::shared_ptr, or any other kind of smart pointer. It's about the importance of using objects to manage resources. auto_ptr and tr1::shared_ptr are just examples of objects that do that. (For more information on tr1:shared_ptr, consult Items 14, 18, and 54.)

Both auto_ptr and tr1::shared_ptr use delete in their destructors, not delete []. (Item 16 describes the difference.) That means that using auto_ptr or TR1::shared_ptr with dynamically allocated arrays is a bad idea, though, regrettably, one that will compile:

 std::auto_ptr<std::string>                       // bad idea! the wrong   aps(new std::string[10]);                      // delete form will be used std::tr1::shared_ptr<int> spi(new int[1024]);    // same problem 

You may be surprised to discover that there is nothing like auto_ptr or TR1::shared_ptr for dynamically allocated arrays in C++, not even in TR1. That's because vector and string can almost always replace dynamically allocated arrays. If you still think it would be nice to have auto_ptr- and TR1::shared_ptr-like classes for arrays, look to Boost (see Item 55). There you'll be pleased to find the boost::scoped_array and boost::shared_array classes that offer the behavior you're looking for.

This Item's guidance to use objects to manage resources suggests that if you're releasing resources manually (e.g., using delete other than in a resource-managing class), you're doing something wrong. Pre-canned resource-managing classes like auto_ptr and tr1::shared_ptr often make following this Item's advice easy, but sometimes you're using a resource where these pre-fab classes don't do what you need. When that's the case, you'll need to craft your own resource-managing classes. That's not terribly difficult to do, but it does involve some subtleties you'll need to consider. Those considerations are the topic of Items 14 and 15.

As a final comment, I have to point out that createInvestment's raw pointer return type is an invitation to a resource leak, because it's so easy for callers to forget to call delete on the pointer they get back. (Even if they use an auto_ptr or tr1::shared_ptr to perform the delete, they still have to remember to store createInvestment's return value in a smart pointer object.) Combatting that problem calls for an interface modification to createInvestment, a topic I address in Item 18.

Things to Remember

  • To prevent resource leaks, use RAII objects that acquire resources in their constructors and release them in their destructors.

  • Two commonly useful RAII classes are TR1::shared_ptr and auto_ptr. tr1::shared_ptr is usually the better choice, because its behavior when copied is intuitive. Copying an auto_ptr sets it to null.

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
Year: 2006
Pages: 102 © 2008-2017.
If you may any questions please contact us: