Multiple inheritance: not such a bad idea

| No Comments

I’m going to try a new approach. Since I’m writing articles at the rate of three and a half every blue moon, I thought I’d recycle material from other places. Here’s one distilled from IRC.

Apparently I’ve acquired a reputation as someone who doesn’t really like the whole object oriented thing. While I never really liked the Kool-Aid, it certainly has its place and it’s often a good way of thinking about a problem. It doesn’t solve everything: nothing does.

Furthermore, it seems that some people have developed the idea that I’m against multiple inheritance, just because I’m down on C++. I usually get thumped by otherwise respectable object-oriented types when I explain that I think multiplie inheritance is actually a pretty neat idea. Oddly enough, they usually think I’m crazy because they’re thinking of C++’s version of multiple inheritance.

C++ is probably the biggest player which actually provides multiple inheritance: later related languages, like Java and C#, have (largely — more on this later) ditched multiple inheritance. And I think the reason is because C++ does such a bad job of it, providing almost none of the features which make it work properly.

So, what’s C++ missing? The most obvious omissions are

  • superclass linearization, and
  • proper method combinations.

Which is nice and technical-sounding.

The most obvious problem is that C++ doesn’t provide a dynamic notion of ‘the next most applicable method’. Consider, for a moment, the classic ‘diamond inheritance’ structure: A is a root class; B and C inherit from A, and D inherits from both B and C. Now, let’s attach a method, frob say, to all of these classes. Each class has some behaviour to perform when an object is frobbed.

So, you write the base class method:

void A::frob(void)
{
  // stuff goes here
}

and all is well. You can now write the methods for classes B and C:

void B::frob(void)
{
  A::frob();
  // special B frobbing
}

void C::frob(void)
{
  // special C frobbing
  if (!all_done) {
    A::frob();
    // further C frobbing
  }
}

So far, so good. Now we get to D.

void D::frob(void)
{
  B::frob();
  C::frob();
  // Maybe specific D stuff
}

Hmm. No, that’s no good. Our object will be A::frobbed twice.

The problem is clear. When your inheritance structure is a simple tree, you can know in advance where you’re supposed to pass the buck. Wherever you are in the tree, when you look up, there’s just a simple path to the root. Multiple inheritance messes all of this up.

Considering class A for a bit: A introduces the frob method, so we can reasonably expect it to be self-contained. The other classes’ implementations are incomplete: they delegate part of their jobs to another method. Looked at in isolation, it’s reasonable for each of them to delegate upwards, towards A. Unfortunately, when D enters the picture, everything goes wrong.

Why? Starting at B and looking up, we can see only A. If we start at D, though, we see both B and C. Since we inherited from both, we presumably wanted both their behaviours. But whichever way we call them both, we’ll call A’s method twice.

One possible solution is to rename frob to dofrob, say, and write a specific frob method on each class which calls the various dofrob methods in the right order. But that’s not quite good enough, because C is actually quite complicated. Even so, we can do something like this:

void C::frob(void)
{
  if (c_start_frobbing()) {
    A::dofrob();
    c_finish_frobbing();
  }
}

This is not going to make D look pretty. In a more complicated inheritance structure, having to call all the superclass-specific hacks in the right ways is going to require a lot of care, and a lot of copying. But wait: isn’t avoiding having to copy code from your superclasses the whole point of inheritance in the first place?

So, we’re in this mess because each method statically ‘knows’ which method to call next, based on which bit of the class structure that method is attached to. The right answer is to replace this static idea of the next method to call at any point by a dynamic idea, determined by the type of the object the method was actually invoked on. (I so don’t want to get into an argument about early and late binding here.) So, how do we decide which order to call the methods?

Well, if Y inherits from X then we’re probably best off calling Y’s method before X’s — if for no other reason that X is less likely to delegate to anyone else. So, in our example, we should call A last. What about B and C? I’d put B before C here, but only because that’s the order I wrote them when explaining the system. (This lets a programmer express ordering requirements by writing superclass lists in a particular way.) So, we do D first, then B, C, and finally A. (More complicated structures don’t fall out so neatly: the superclass structure and sibling ordering rules I’ve mentioned aren’t enough to pick an unambiguous ordering. There are a number of different disambiguation rules, but they don’t really matter right now.)

So, that’s what I meant when I was talking about superclass linearization. What about the method combinations? I think I’ll leave that for another article.

Finally, I noted that Java and C# haven’t completely ditched MI: they get off on a technicality. ‘Interfaces’, in the style of Java and C# provide no state and no behaviour; they’re just a hack for the type system. It’s interesting that MI continues to do fairly well specifically among dynamically typed languages, e.g., Lisp, Python, Dylan, Dave Moon’s new PLOT, where interfaces would do no good anyway. So there must be some use, right?

Leave a comment

About this Entry

This page contains a single entry by Mark Wooding published on April 17, 2009 5:51 PM.

Send me encrypted mail! was the previous entry in this blog.

mdup is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.

Pages

OpenID accepted here Learn more about OpenID
Powered by Movable Type 5.2.13