Session 17: Multiple inheritance

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.

Ambiguity

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.

Professor *prof = new Professor();
prof->talk();
It's ambiguous as to what would happen here: Does Professor inherit Worker's talk method or SmartPerson's talk method?

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.

((Worker*) prof)->talk();
((SmartPerson*) prof)->talk();
As C++ resolves the problem, however, the Professor says two different things. This doesn't seem like a satisfying approach.

The diamond problem

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.

Person *who = new Professor(); // will not compile
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.

Virtual inheritance

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.

Implementation issues

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.