More Books
Effective C++ 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Table of Contents
Copyright
Praise for Effective C++, Third Edition
Addison-Wesley Professional Computing Series
Preface
Acknowledgments
Introduction
Terminology
Chapter 1. Accustoming Yourself to C++
Item 1: View C++ as a federation of languages
Item 2: Prefer consts, enums, and inlines to #defines
Item 3: Use const whenever possible
Item 4: Make sure that objects are initialized before they're used
Chapter 2. Constructors, Destructors, and Assignment Operators
Item 5: Know what functions C++ silently writes and calls
Item 6: Explicitly disallow the use of compiler-generated functions you do not want
Item 7: Declare destructors virtual in polymorphic base classes
Item 8: Prevent exceptions from leaving destructors
Item 9: Never call virtual functions during construction or destruction
Item 10: Have assignment operators return a reference to *this
Item 11: Handle assignment to self in operator=
Item 12: Copy all parts of an object
Chapter 3. Resource Management
Item 13: Use objects to manage resources.
Item 14: Think carefully about copying behavior in resource-managing classes.
Item 15: Provide access to raw resources in resource-managing classes.
Item 16: Use the same form in corresponding uses of new and delete.
Item 17: Store newed objects in smart pointers in standalone statements.
Chapter 4. Designs and Declarations
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
Item 19: Treat class design as type design
Item 20: Prefer pass-by-reference-to-const to pass-by-value
Item 21: Don't try to return a reference when you must return an object
Item 22: Declare data members private
Item 23: Prefer non-member non-friend functions to member functions
Item 24: Declare non-member functions when type conversions should apply to all parameters
Item 25: Consider support for a non-throwing swap
Chapter 5. Implementations
Item 26: Postpone variable definitions as long as possible.
Item 27: Minimize casting.
Item 28: Avoid returning "handles" to object internals.
Item29: Strive for exception-safe code.
Item 30: Understand the ins and outs of inlining.
Item31: Minimize compilation dependencies between files.
Chapter 6. Inheritance and Object-Oriented Design
Item 32: Make sure public inheritance models "is-a."
Item 33: Avoid hiding inherited names
Item 34: Differentiate between inheritance of interface and inheritance of implementation
Item 35: Consider alternatives to virtual functions
Item 36: Never redefine an inherited non-virtual function
Item 37: Never redefine a function's inherited default parameter value
Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition
Item 39: Use private inheritance judiciously
Item 40: Use multiple inheritance judiciously
Chapter 7. Templates and Generic Programming
Item 41: Understand implicit interfaces and compile-time polymorphism
Item 42: Understand the two meanings of typename
Item 43: Know how to access names in templatized base classes
Item 44: Factor parameter-independent code out of templates
Item 45: Use member function templates to accept "all compatible types."
Item 46: Define non-member functions inside templates when type conversions are desired
Item 47: Use traits classes for information about types
Item 48: Be aware of template metaprogramming
Chapter 8. Customizing new and delete
Item 49: Understand the behavior of the new-handler
Item 50: Understand when it makes sense to replace new and delete
Item 51: Adhere to convention when writing new and delete
Item 52: Write placement delete if you write placement new
Chapter 9. Miscellany
Item 53: Pay attention to compiler warnings.
Item 54: Familiarize yourself with the standard library, including TR1
Item.55: Familiarize yourself with Boost.
Appendix A. Beyond Effective C++
Appendix B. Item Mappings Between Second and Third Editions
Index
index_SYMBOL
index_A
index_B
index_C
index_D
index_E
index_F
index_G
index_H
index_I
index_J
index_K
index_L
index_M
index_N
index_O
index_P
index_R
index_S
index_T
index_U
index_V
index_W
index_X
index_Z

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.