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 40: Use multiple inheritance judiciously

When it comes to multiple inheritance (MI), the C++ community largely breaks into two basic camps. One camp believes that if single inheritance (SI) is good, multiple inheritance must be better. The other camp argues that single inheritance is good, but multiple inheritance isn't worth the trouble. In this Item, our primary goal is to understand both perspectives on the MI question.

One of the first things to recognize is that when MI enters the designscape, it becomes possible to inherit the same name (e.g., function, typedef, etc.) from more than one base class. That leads to new opportunities for ambiguity. For example:


class BorrowableItem {             // something a library lets you borrow

public:

  void checkOut();                 // check the item out from the library



  ...

};



class ElectronicGadget {

private:

  bool checkOut() const;           // perform self-test, return whether



  ...                              // test succeeds

};



class MP3Player:                   // note MI here

  public BorrowableItem,           // (some libraries loan MP3 players)

  public ElectronicGadget

{ ... };                           // class definition is unimportant

MP3Player mp;



mp.checkOut();                     // ambiguous! which checkOut?


Note that in this example, the call to checkOut is ambiguous, even though only one of the two functions is accessible. (checkOut is public in BorrowableItem but private in ElectronicGadget.) That's in accord with the C++ rules for resolving calls to overloaded functions: before seeing whether a function is accessible, C++ first identifies the function that's the best match for the call. It checks accessibility only after finding the best-match function. In this case, both checkOuts are equally good matches, so there's no best match. The accessibility of ElectronicGadget::checkOut is therefore never examined.

To resolve the ambiguity, you must specify which base class's function to call:


mp.BorrowableItem::checkOut();              // ah, that checkOut...


You could try to explicitly call ElectronicGadget::checkOut, too, of course, but then the ambiguity error would be replaced with a "you're trying to call a private member function" error.

Multiple inheritance just means inheriting from more than one base class, but it is not uncommon for MI to be found in hierarchies that have higher-level base classes, too. That can lead to what is sometimes known as the "deadly MI diamond"


class File { ... };

class InputFile: public File { ... };

class OutputFile: public File { ... };

class IOFile: public InputFile,

              public OutputFile

{ ... };


Any time you have an inheritance hierarchy with more than one path between a base class and a derived class (such as between File and IOFile above, which has paths through both InputFile and OutputFile), you must confront the question of whether you want the data members in the base class to be replicated for each of the paths. For example, suppose that the File class has a data member, fileName. How many copies of this field should IOFile have? On the one hand, it inherits a copy from each of its base classes, so that suggests that IOFile should have two fileName data members. On the other hand, simple logic says that an IOFile object has only one file name, so the fileName field it inherits through its two base classes should not be replicated.

C++ takes no position on this debate. It happily supports both options, though its default is to perform the replication. If that's not what you want, you must make the class with the data (i.e., File) a virtual base class. To do that, you have all classes that immediately inherit from it use virtual inheritance:


class File { ... };

class InputFile: virtual public File { ... };

class OutputFile: virtual public File { ... };

class IOFile: public InputFile,

              public OutputFile

{ ... };


The standard C++ library contains an MI hierarchy just like this one, except the classes are class templates, and the names are basic_ios, basic_istream, basic_ostream, and basic_iostream instead of File, InputFile, OutputFile, and IOFile.

From the viewpoint of correct behavior, public inheritance should always be virtual. If that were the only point of view, the rule would be simple: anytime you use public inheritance, use virtual public inheritance. Alas, correctness is not the only perspective. Avoiding the replication of inherited fields requires some behind-the-scenes legerdemain on the part of compilers, and the result is that objects created from classes using virtual inheritance are generally larger than they would be without virtual inheritance. Access to data members in virtual base classes is also slower than to those in non-virtual base classes. The details vary from compiler to compiler, but the basic thrust is clear: virtual inheritance costs.

It costs in other ways, too. The rules governing the initialization of virtual base classes are more complicated and less intuitive than are those for non-virtual bases. The responsibility for initializing a virtual base is borne by the most derived class in the hierarchy. Implications of this rule include (1) classes derived from virtual bases that require initialization must be aware of their virtual bases, no matter how far distant the bases are, and (2) when a new derived class is added to the hierarchy, it must assume initialization responsibilities for its virtual bases (both direct and indirect).

My advice on virtual base classes (i.e., on virtual inheritance) is simple. First, don't use virtual bases unless you need to. By default, use non-virtual inheritance. Second, if you must use virtual base classes, try to avoid putting data in them. That way you won't have to worry about oddities in the initialization (and, as it turns out, assignment) rules for such classes. It's worth noting that Interfaces in Java and .NET, which are in many ways comparable to virtual base classes in C++, are not allowed to contain any data.

Let us now turn to the following C++ Interface class (see Item31) for modeling persons:


class IPerson {

public:

  virtual ~IPerson();



  virtual std::string name() const = 0;

  virtual std::string birthDate() const = 0;

};


IPerson clients must program in terms of IPerson pointers and references, because abstract classes cannot be instantiated. To create objects that can be manipulated as IPerson objects, clients of IPerson use factory functions (again, see Item 31) to instantiate concrete classes derived from IPerson:


// factory function to create a Person object from a unique database ID;

// see Item 18 for why the return type isn't a raw pointer

std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);



// function to get a database ID from the user

DatabaseID askUserForDatabaseID();





DatabaseID id(askUserForDatabaseID());

std::tr1::shared_ptr<IPerson> pp(makePerson(id));    // create an object

                                                     // supporting the

                                                     // IPerson interface



...                                                  // manipulate *pp via

                                                     // IPerson's member

                                                     // functions


But how does makePerson create the objects to which it returns pointers? Clearly, there must be some concrete class derived from IPerson that makePerson can instantiate.

Suppose this class is called CPerson. As a concrete class, CPerson must provide implementations for the pure virtual functions it inherits from IPerson. It could write these from scratch, but it would be better to take advantage of existing components that do most or all of what's necessary. For example, suppose an old database-specific class PersonInfo offers the essence of what CPerson needs:


class PersonInfo {

public:

  explicit PersonInfo(DatabaseID pid);

  virtual ~PersonInfo();



  virtual const char * theName() const;

  virtual const char * theBirthDate() const;

  ...



private:

  virtual const char * valueDelimOpen() const;      // see

  virtual const char * valueDelimClose() const;     // below

  ...

};


You can tell this is an old class, because the member functions return const char*s instead of string objects. Still, if the shoe fits, why not wear it? The names of this class's member functions suggest that the result is likely to be pretty comfortable.

You come to discover that PersonInfo was designed to facilitate printing database fields in various formats, with the beginning and end of each field value delimited by special strings. By default, the opening and closing delimiters for field values are square brackets, so the field value "Ring-tailed Lemur" would be formatted this way:


[Ring-tailed Lemur]


In recognition of the fact that square brackets are not universally desired by clients of PersonInfo, the virtual functions valueDelimOpen and valueDelimClose allow derived classes to specify their own opening and closing delimiter strings. The implementations of PersonInfo's member functions call these virtual functions to add the appropriate delimiters to the values they return. Using PersonInfo::theName as an example, the code looks like this:


const char * PersonInfo::valueDelimOpen() const

{

  return "[";                       // default opening delimiter

}



const char * PersonInfo::valueDelimClose() const

{

  return "]";                       // default closing delimiter

}



const char * PersonInfo::theName() const

{

  // reserve buffer for return value; because this is

  // static, it's automatically initialized to all zeros

  static char value[Max_Formatted_Field_Value_Length];



  // write opening delimiter

  std::strcpy(value, valueDelimOpen());



  append to the string in value this object's   name field (being careful

  to avoid buffer overruns!)



  // write closing delimiter

  std::strcat(value, valueDelimClose());



  return value;

}


One might question the antiquated design of PersonInfo::theName (especially the use of a fixed-size static buffer, something that's rife for both overrun and threading problems — see also Item21), but set such questions aside and focus instead on this: theName calls valueDelimOpen to generate the opening delimiter of the string it will return, then it generates the name value itself, then it calls valueDelimClose.

Because valueDelimOpen and valueDelimClose are virtual functions, the result returned by theName is dependent not only on PersonInfo but also on the classes derived from PersonInfo.

As the implementer of CPerson, that's good news, because while perusing the fine print in the IPerson documentation, you discover that name and birthDate are required to return unadorned values, i.e., no delimiters are allowed. That is, if a person is named Homer, a call to that person's name function should return "Homer", not "[Homer]".

The relationship between CPerson and PersonInfo is that PersonInfo happens to have some functions that would make CPerson easier to implement. That's all. Their relationship is thus is-implemented-in-terms-of, and we know that can be represented in two ways: via composition (see Item 38) and via private inheritance (see Item 39). Item 39 points out that composition is the generally preferred approach, but inheritance is necessary if virtual functions are to be redefined. In this case, CPerson needs to redefine valueDelimOpen and valueDelimClose, so simple composition won't do. The most straightforward solution is to have CPerson privately inherit from PersonInfo, though Item 39 explains that with a bit more work, CPerson could also use a combination of composition and inheritance to effectively redefine PersonInfo's virtuals. Here, we'll use private inheritance.

But CPerson must also implement the IPerson interface, and that calls for public inheritance. This leads to one reasonable application of multiple inheritance: combine public inheritance of an interface with private inheritance of an implementation:


class IPerson {                            // this class specifies the

public:                                    // interface to be implemented

  virtual ~IPerson();



  virtual std::string name() const = 0;

  virtual std::string birthDate() const = 0;

};



class DatabaseID { ... };                  // used below; details are

                                           // unimportant



class PersonInfo {                         // this class has functions

public:                                    // useful in implementing

  explicit PersonInfo(DatabaseID pid);     // the IPerson interface

  virtual ~PersonInfo();



  virtual const char * theName() const;

  virtual const char * theBirthDate() const;



  virtual const char * valueDelimOpen() const;

  virtual const char * valueDelimClose() const;

  ...

};

class CPerson: public IPerson, private PersonInfo {     // note use of MI

public:

  explicit CPerson(    DatabaseID pid): PersonInfo(pid) {}

  virtual std::string name() const                      // implementations

  { return PersonInfo::theName(); }                     // of the required

                                                        // IPerson member

  virtual std::string birthDate() const                 // functions

  { return PersonInfo::theBirthDate(); }

private:                                                // redefinitions of

  const char * valueDelimOpen() const { return ""; }    // inherited virtual

  const char * valueDelimClose() const { return ""; }   // delimiter

};                                                      // functions


In UML, the design looks like this:

This example demonstrates that MI can be both useful and comprehensible.

At the end of the day, multiple inheritance is just another tool in the object-oriented toolbox. Compared to single inheritance, it's typically more complicated to use and more complicated to understand, so if you've got an SI design that's more or less equivalent to an MI design, the SI design is almost certainly preferable. If the only design you can come up with involves MI, you should think a little harder — there's almost certainly some way to make SI work. At the same time, MI is sometimes the clearest, most maintainable, most reasonable way to get the job done. When that's the case, don't be afraid to use it. Just be sure to use it judiciously.

Things to Remember

  • Multiple inheritance is more complex than single inheritance. It can lead to new ambiguity issues and to the need for virtual inheritance.

  • Virtual inheritance imposes costs in size, speed, and complexity of initialization and assignment. It's most practical when virtual base classes have no data.

  • Multiple inheritance does have legitimate uses. One scenario involves combining public inheritance from an Interface class with private inheritance from a class that helps with implementation.