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 18: Make interfaces easy to use correctly and hard to use incorrectly

C++ is awash in interfaces. Function interfaces. Class interfaces. Template interfaces. Each interface is a means by which clients interact with your code. Assuming you're dealing with reasonable people, those clients are trying to do a good job. They want to use your interfaces correctly. That being the case, if they use one incorrectly, your interface is at least partially to blame. Ideally, if an attempted use of an interface won't do what the client expects, the code won't compile; and if the code does compile, it will do what the client wants.

Developing interfaces that are easy to use correctly and hard to use incorrectly requires that you consider the kinds of mistakes that clients might make. For example, suppose you're designing the constructor for a class representing dates in time:


class Date {

public:

  Date(int month, int day, int year);

  ...

};


At first glance, this interface may seem reasonable (at least in the USA), but there are at least two errors that clients might easily make. First, they might pass parameters in the wrong order:


Date d(30, 3, 1995);              // Oops! Should be "3, 30" , not "30, 3"


Second, they might pass an invalid month or day number:


Date d(2, 20, 1995);              // Oops! Should be "3, 30" , not "2, 20"


(This last example may look silly, but remember that on a keyboard, 2 is next to 3. Such "off by one" typing errors are not uncommon.)

Many client errors can be prevented by the introduction of new types. Indeed, the type system is your primary ally in preventing undesirable code from compiling. In this case, we can introduce simple wrapper types to distinguish days, months, and years, then use these types in the Date constructor:


struct Day {            struct Month {                struct Year {

  explicit Day(int d)     explicit Month(int m)         explicit Year(int y)

  :val(d) {}              :val(m) {}                    :val(y){}



  int val;                int val;                      int val;

};                      };                            };

class Date {

public:

 Date(const Month& m, const Day& d, const Year& y);

 ...

};

Date d(30, 3, 1995);                      // error! wrong types



Date d(Day(30), Month(3), Year(1995));    // error! wrong types



Date d(Month(3), Day(30), Year(1995));    // okay, types are correct


Making Day, Month, and Year full-fledged classes with encapsulated data would be better than the simple use of structs above (see Item 22), but even structs suffice to demonstrate that the judicious introduction of new types can work wonders for the prevention of interface usage errors.

Once the right types are in place, it can sometimes be reasonable to restrict the values of those types. For example, there are only 12 valid month values, so the Month type should reflect that. One way to do this would be to use an enum to represent the month, but enums are not as type-safe as we might like. For example, enums can be used like ints (see Item 2). A safer solution is to predefine the set of all valid Months:


class Month {

public:

  static Month Jan() { return Month(1); }   // functions returning all valid

  static Month Feb() { return Month(2); }   // Month values; see below for

  ...                                       // why these are functions, not

  static Month Dec() { return Month(12); }  // objects

  

  ...                                       // other member functions



private:

  explicit Month(int m);                    // prevent creation of new

                                            // Month values



  ...                                       // month-specific data

};

Date d(Month::Mar(), Day(30), Year(1995));


If the idea of using functions instead of objects to represent specific months strikes you as odd, it may be because you have forgotten that reliable initialization of non-local static objects can be problematic. Item 4 can refresh your memory.

Another way to prevent likely client errors is to restrict what can be done with a type. A common way to impose restrictions is to add const. For example, Item 3 explains how const-qualifying the return type from operator* can prevent clients from making this error for user-defined types:


if (a * b = c) ...                    // oops, meant to do a comparison!


In fact, this is just a manifestation of another general guideline for making types easy to use correctly and hard to use incorrectly: unless there's a good reason not to, have your types behave consistently with the built-in types. Clients already know how types like int behave, so you should strive to have your types behave the same way whenever reasonable. For example, assignment to a*b isn't legal if a and b are ints, so unless there's a good reason to diverge from this behavior, it should be illegal for your types, too. When in doubt, do as the ints do.

The real reason for avoiding gratuitous incompatibilities with the built-in types is to offer interfaces that behave consistently. Few characteristics lead to interfaces that are easy to use correctly as much as consistency, and few characteristics lead to aggravating interfaces as much as inconsistency. The interfaces to STL containers are largely (though not perfectly) consistent, and this helps make them fairly easy to use. For example, every STL container has a member function named size that tells how many objects are in the container. Contrast this with Java, where you use the length property for arrays, the length method for Strings, and the size method for Lists; and with .NET, where Arrays have a property named Length, while ArrayLists have a property named Count. Some developers think that integrated development environments (IDEs) render such inconsistencies unimportant, but they are mistaken. Inconsistency imposes mental friction into a developer's work that no IDE can fully remove.

Any interface that requires that clients remember to do something is prone to incorrect use, because clients can forget to do it. For example, Item 13 introduces a factory function that returns pointers to dynamically allocated objects in an Investment hierarchy:


Investment* createInvestment();   // from Item 13; parameters omitted

                                  // for simplicity


To avoid resource leaks, the pointers returned from createInvestment must eventually be deleted, but that creates an opportunity for at least two types of client errors: failure to delete a pointer, and deletion of the same pointer more than once.

Item 13 shows how clients can store createInvestment's return value in a smart pointer like auto_ptr or tr1::shared_ptr, thus turning over to the smart pointer the responsibility for using delete. But what if clients forget to use the smart pointer? In many cases, a better interface decision would be to preempt the problem by having the factory function return a smart pointer in the first place:


std::tr1::shared_ptr<Investment> createInvestment();


This essentially forces clients to store the return value in a TR1::shared_ptr, all but eliminating the possibility of forgetting to delete the underlying Investment object when it's no longer being used.

In fact, returning a TR1::shared_ptr makes it possible for an interface designer to prevent a host of other client errors regarding resource release, because, as Item 14 explains, TR1::shared_ptr allows a resource-release function — a "deleter" — to be bound to the smart pointer when the smart pointer is created. (auto_ptr has no such capability.)

Suppose clients who get an Investment* pointer from createInvestment are expected to pass that pointer to a function called geTRidOfInvestment instead of using delete on it. Such an interface would open the door to a new kind of client error, one where clients use the wrong resource-destruction mechanism (i.e., delete instead of getridOfInvestment). The implementer of createInvestment can forestall such problems by returning a TR1::shared_ptr with geTRidOfInvestment bound to it as its deleter.

tr1::shared_ptr offers a constructor taking two arguments: the pointer to be managed and the deleter to be called when the reference count goes to zero. This suggests that the way to create a null tr1::shared_ptr with getridOfInvestment as its deleter is this:


std::tr1::shared_ptr<Investment>      // attempt to create a null

  pInv(0, getRidOfInvestment);        // shared_ptr with a custom deleter;

                                      // this won't compile


Alas, this isn't valid C++. The TR1::shared_ptr constructor insists on its first parameter being a pointer, and 0 isn't a pointer, it's an int. Yes, it's convertible to a pointer, but that's not good enough in this case; tr1::shared_ptr insists on an actual pointer. A cast solves the problem:


std::tr1::shared_ptr<Investment>      // create a null shared_ptr with

  pInv(static_cast<Investment*>(0),   // getRidOfInvestment as its

        getRidOfInvestment);          // deleter; see Item 27 for info on

                                      // static_cast


This means that the code for implementing createInvestment to return a tr1::shared_ptr with geTRidOfInvestment as its deleter would look something like this:


std::tr1::shared_ptr<Investment> createInvestment()

{

  std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),

                                          getRidOfInvestment);



  retVal = ... ;                         // make retVal point to the

                                         // correct object



  return retVal;

}


Of course, if the raw pointer to be managed by pInv could be determined prior to creating pInv, it would be better to pass the raw pointer to pInv's constructor instead of initializing pInv to null and then making an assignment to it. For details on why, consult Item 26.

An especially nice feature of tr1::shared_ptr is that it automatically uses its per-pointer deleter to eliminate another potential client error, the "cross-DLL problem." This problem crops up when an object is created using new in one dynamically linked library (DLL) but is deleted in a different DLL. On many platforms, such cross-DLL new/delete pairs lead to runtime errors. tr1::shared_ptr avoids the problem, because its default deleter uses delete from the same DLL where the tr1::shared_ptr is created. This means, for example, that if Stock is a class derived from Investment and createInvestment is implemented like this,


std::tr1::shared_ptr<Investment> createInvestment()

{

  return std::tr1::shared_ptr<Investment>(new Stock);

}


the returned tr1::shared_ptr can be passed among DLLs without concern for the cross-DLL problem. The tr1::shared_ptrs pointing to the Stock keep track of which DLL's delete should be used when the reference count for the Stock becomes zero.

This Item isn't about tr1::shared_ptr — it's about making interfaces easy to use correctly and hard to use incorrectly — but tr1::shared_ptr is such an easy way to eliminate some client errors, it's worth an overview of the cost of using it. The most common implementation of tr1::shared_ptr comes from Boost (see Item 55). Boost's shared_ptr is twice the size of a raw pointer, uses dynamically allocated memory for bookkeeping and deleter-specific data, uses a virtual function call when invoking its deleter, and incurs thread synchronization overhead when modifying the reference count in an application it believes is multithreaded. (You can disable multithreading support by defining a preprocessor symbol.) In short, it's bigger than a raw pointer, slower than a raw pointer, and uses auxiliary dynamic memory. In many applications, these additional runtime costs will be unnoticeable, but the reduction in client errors will be apparent to everyone.

Things to Remember

  • Good interfaces are easy to use correctly and hard to use incorrectly. Your should strive for these characteristics in all your interfaces.

  • Ways to facilitate correct use include consistency in interfaces and behavioral compatibility with built-in types.

  • Ways to prevent errors include creating new types, restricting operations on types, constraining object values, and eliminating client resource management responsibilities.

  • TR1::shared_ptr supports custom deleters. This prevents the cross-DLL problem, can be used to automatically unlock mutexes (see Item 14), etc.