Effective C++ 笔记 —— Item 40: Use multiple inheritance judiciously.
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 some times 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.
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 { /*...*/ };
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).
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.
Combine public inheritance of an interface with private inheritance of an implementation:
class IPerson { // this class specifies the interface to be implemented public: 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 useful in implementing public: explicit PersonInfo(DatabaseID pid); // the IPerson interface virtual ~PersonInfo(); virtual const char * theName() const; virtual const char * theBirthDate() const; // ... private: 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 of the required IPerson member functions { return PersonInfo::theName(); } virtual std::string birthDate() const { return PersonInfo::theBirthDate(); } private: // redefinitions of const char * valueDelimOpen() const { return ""; } // inherited virtual delimiter functions const char * valueDelimClose() const { return ""; } };
In UML, the design looks like this:
This example demonstrates that MI can be both useful and comprehensible.
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.