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.
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 { };
Here we have declared Professor to be a subclass of both Worker and
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.
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 { };
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.)
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.
class Person {
public: int brain;
Person(int smartness) { brain = smartness }
};
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 Worker : virtual public Person {
public: Worker() : Person(20) { }
void talk() { brain--; }
};
class SmartPerson : virtual public Person {
public: SmartPerson() : Person(100) { }
void talk() { brain++; }
};
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 Professor : public Worker, public SmartPerson {
public: Professor() : Worker(), SmartPerson() { } // will not compile
};
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?
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.
class Professor : public Worker, public SmartPerson {
public: Professor() : Person(5000), Worker(), SmartPerson() { } // will not compile
};
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.
One difficulty arising from virtual inheritance is in developing the way in which records are laid out. Suppose we have the following declarations.
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 { };
In compiling this, the compiler will want to decide on the following CIR
formats for the classes.
Person CIR Worker CIR SmartPerson CIR
0: brain 0: brain 0: brain
4: pay 4: pithiness
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.
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.
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
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.
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.