Search code examples
inheritancemultiple-inheritancediamond-problem

Diamond vs triangle inheritance


My question is the same as this old one, but I do not yet understand the answer given: Diamond Problem

In the diamond problem, D inherits from B and C which both inherit from A, and B and C both override a method foo in A.

Suppose instead a triangle: there is no A, D inherits from B and C, which both implement a method foo.

As I understand, without special handling, the following would be ambiguous in either the diamond or triangle because it is not clear which foo to call:

D d = new D;
d.foo();

So I'm still not sure what makes this a diamond problem and not a more general multiple inheritance problem. It seems like you would need to provide some way to disambiguate this even in the "triangle" problem.


Solution

  • As I alluded to in the comments, some of the issues relate to how dynamic dispatch is typically implemented.

    Assuming a vtable approach, any particular type must be able to produce a vtable that allows it to be treated as itself or any of its supertypes. Under single inheritance, this can be really easily implemented since each type's vtable can start with the same vtable layout as its immediate supertype followed by any new members that it introduces.

    E.g. If B has two methods

    vtable_B
    Slot #       Method
    1            B.foo
    2            B.bar
    

    And D inherits from B, overrides bar and introduces baz:

    vtable_SI_D
    Slot #       Method
    1            B.foo
    2            D.bar
    3            D.baz
    

    Since D didn't override foo, it just copies whatever entry it finds in Bs vtable for slot #1.

    Then any code working with a D through a B variable will only ever use slots #1 and #2 and everything works fine.

    Introduce multiple inheritance, however, and you may not be able to use a single vtable. Assume we now introduce C which also has foo and bar methods. Now we'll need to use different vtables when D is cast to B:

    vtable_MI_D_as_B
    Slot #       Method
    1            B.foo
    2            D.bar
    

    or to C:

    vtable_MI_D_as_C
    Slot #       Method
    1            C.foo
    2            D.bar
    

    These are unambiguous1. The issue is trying to fill in the vtable for D when its not cast to anything:

    Slot #       Method
    1            <what goes here>
    2            D.bar
    3            D.baz
    

    So, you're correct that the triangle inheritance does raise some issues. But since we're using a different vtable for D as D (as opposed to D as B or C) we could simply omit an entry for Slot #1 and make it illegal to call D.foo (in the simple case that nothing further is stated in Ds definition such as to use Bs foo or overriding foo):

    vtable_MI_D
    Slot #       Method
    2            D.bar
    3            D.baz
    

    Let's now introduce A and have it define foo, back to the classic diamond pattern. So As vtable is:

    vtable_A
    Slot #       Method
    1            A.foo
    

    B and C are as described above. We can follow exactly the same approach above for D, except for one additional problem. We have to supply a vtable for D cast as A. We can't just omit slot #1 - code dealing with an A expects to be able to call foo. And we can't just copy the entry from B or C's vtable since they have different values and they're both immediate supertypes.

    This, I believe, is the gist of why the diamond pattern is typically used - because we can't just implement a "you can't call foo on a D" rule and be done with it.


    1It's also worth observing here that slots #1 and #2 in the vtable_MI_D_as_B and vtable_MI_D_as_C vtables are completely unrelated. C could have had slot #2 be for its foo method and slot #6 for its bar method. Method with the same names won't necessarily share the "same" slots.

    This is in contrast with the later discussion of the diamond inheritance pattern where slot #1 really is the same slot across all types.