We looked at the material in Sections 9.2, 9.3, 9.6, 9.7 in the textbook. Then we looked at some of the complexities surrounding multiple inheritance.
C++, which allows multiple inheritance, allows us to write the following.
Here we have declared Professor to be a subclass of both Worker and SmartPerson.class Worker { public: void talk() { System.out.println("Off to work we go."); } }; class SmartPerson { public: void talk() { System.out.println("I'm pithy."); } }; class Professor : public Worker, public SmartPerson { };
Now suppose we were to write the following using this class.
It's ambiguous as to what would happen here: Does Professor inherit Worker's talk method or SmartPerson's talk method?Professor *prof = new Professor(); prof->talk();
C++ resolves this conflict by prohibiting the compiler from accepting a call to the talk method. This sounds like a simple solution, but it's not so simple as that. Consider the following.
As C++ resolves the problem, however, the Professor says two different things. This doesn't seem like a satisfying approach.((Worker*) prof)->talk(); ((SmartPerson*) prof)->talk();
Problems get worse when you notice that multiple inheritance means that a class could extend the same class multiple times.
Professor includes both a Worker and a Professor as sub-objects, C++ would say, and Worker includes a Person and Professor includes a Person. Thus, Professor includes two Persons. (This reveals, to a certain extent, that C++ designers believed that inheritance should be used for ``buying'' objects.)class Person { public: int brain; }; class Worker : public Person { public: void talk() { brain--; } }; class SmartPerson : public Person { public: void talk() { brain++; } }; class Professor : public Worker, public SmartPerson { };
This choice that each Professor in this case has two Person sub-objects raises some issues that aren't immediately obvious. One notable issue is that we can no longer always cast up the class hierarchy: Suppose we try to cast a Professor into a Person, as in the following.
Which Person sub-object of the newly created Professor object should who reference? C++'s answer is neither one: C++ requires the compiler to output an error for this line.Person *who = new Professor(); // will not compile
One thing you'll notice in the above diamond example is that each Professor, which has two Person sub-objects, therefore has two brain variables. This may strike you as somewhat peculiar: Which should a professor have two brains? Why doesn't C++ merge the two?
C++ has a way of doing this: What we looked at earlier, the default behavior, is called regular inheritance. But there is also virtual inheritance; there can only be one sub-object of a class that is virtually extended.
To designate virtual inheritance, you can use the virtual keyword.
class Person { public: int brain; }; class Worker : virtual public Person { public: void talk() { brain--; } }; class SmartPerson : virtual public Person { public: void talk() { brain++; } }; class Professor : public Worker, public SmartPerson { };
Virtual inheritance is more problematic than regular inheritance. For example: Suppose we want to have a constructor method for Person that initializes brain to a value.
Since this is the only constructor method, subclasses would need to have a constructor method too to specify what to pass into the Person constructor method.class Person { public: int brain; Person(int smartness) { brain = smartness } };
Here we've specified that a newly created Worker passes 20 to the constructor method for Person, which would initialize the brain instance variable to 20. A SmartPerson, on the other hand, would pass 100 to Person's constructor method. So far, so good. But what about Professor's constructor method? As you would expect, we would specify which constructor method to use for each of the superclasses.class Worker : virtual public Person { public: Worker() : Person(20) { } void talk() { brain--; } }; class SmartPerson : virtual public Person { public: SmartPerson() : Person(100) { } void talk() { brain++; } };
But this won't compile: The problem arises in constructing the Person sub-object: Worker wants to pass 20 to Person's constructor method, and SmartPerson wants to pass 100. How does C++ resolve this conflict?class Professor : public Worker, public SmartPerson { public: Professor() : Worker(), SmartPerson() { } // will not compile };
C++ chooses, again, to simply say that this is illegal. Instead, C++ says, you must specify what you want to pass to Person's constructor method in Professor's constructor method.
Here, we construct the Person sub-object passing 5000 as the parameter. In performing the Worker and SmartPerson constructor method, it bypasses calling the Person sub-object's constructor method a second time.class Professor : public Worker, public SmartPerson { public: Professor() : Person(5000), Worker(), SmartPerson() { } // will not compile };
One difficulty arising from virtual inheritance is in developing the way in which records are laid out. Suppose we have the following declarations.
In compiling this, the compiler will want to decide on the following CIR formats for the classes.class Person { public: int brain; }; class Worker : virtual public Person { public: int pay; int talk() { brain += pay; } }; class SmartPerson : virtual public Person { public: int pithiness; int talk() { brain += pithiness; } }; class Professor : public Worker, public SmartPerson { };
It will want to compile the methods accordingly, so that Worker's talk method says to take this (the Worker performing the talk method) and look 0 bytes in to get brain and increase it by the value found at 4 bytes into this, which would be pay. SmartPerson's talk method would compile similarly.Person CIR Worker CIR SmartPerson CIR 0: brain 0: brain 0: brain 4: pay 4: pithiness
This looks great until you consider what Professor's CIR should be. It will place brain at offset 0, but it wants to place both pay and pithiness at offset 4 in order to be compatible, respectfully, with Worker's talk method and SmartPerson's talk method. The compiler is stuck.
One approach to solving this is to change one of the CIR's to include an intentional gap in either Worker or SmartPerson to accommodate the possibility that a class inherits from both.
Now, SmartPerson's talk method would expect pithiness at offset 8. That's fine, even if we're actually using the method on a Professor.Person CIR Worker CIR SmartPerson CIR Professor CIR 0: brain 0: brain 0: brain 0: brain 4: pay 4: --unused-- 4: pay 8: pithiness 8: pithiness
Placing such gaps in the CIR is irritating, however. After all, there could be many SmartPersons, and all of them will have a wasted four bytes in the middle. There are ways of avoiding this waste of memory, but they're complicated and they are slower.