Best language for solving diamond problem & is this a good idea for multiple inheritance?

Started by
13 comments, last by Hodgman 8 years, 7 months ago

Composition can probably eliminate the need for almost any form of inheritance, however using inheritance mostly makes code shorter and less repetitive, imagine an insanely rare case of a triple occurrence of the diamond problem with multiple inheritance, using composition would usually lead to you having to do things like instanceOfClassA.instanceOfClassB.instanceofClassC.instanceOfClassD for every function or variable you're going to use. Although I should expect this never to happen in my lifetime, repetition, how ever little, in my opinion, should always be eliminated.

Both the composition and inheritance exist for a reason. Overusing one over the other is not a good idea either. Beside of those there are also other patterns and principles that makes developer's life easier, especially "single responsibility" and KISS (keep it simple stupid).

If you run into situation in your code that you need to use something like instanceOfClassA.instanceOfClassB.instanceofClassC.instanceOfClassD, it is clear sign you are doing too much in one class. And the same goes for multiple inheritance.

There is reason that many modern languages do not allow for multiple inheritance. It is not because it is hard to implement in compiler (and I believe it is hard ;)), but because it is confusing for developers thus error prone.

Advertisement

There is reason that many modern languages do not allow for multiple inheritance. It is not because it is hard to implement in compiler (and I believe it is hard ;)), but because it is confusing for developers thus error prone.

I think the reason is interfaces, which go a long way towards eliminating the need for multiple inheritance.
The problem that gets solved in both cases is the idea that you can see a single object in multiple ways (ie talk to it in different ways to the same object).

C++ uses inheritance to define how you can talk to an object.
Java (and C# too, it seems) uses interfaces. I don't know C#, but Java has single inheritance, and you add as many interfaces as you like to define how you can talk to an object.

Interfaces are a bit more flexible, in the sense that you're not stuck to the inheritance hierarchy.
On the other hand, interfaces (of Java at least) are less powerful by not allow adding data or implementations. (Iirc work is being done (or has been done) to allow a very simple form of implementations though.) This means you cannot add a base implementation through an interface in Java.

I think the reason is interfaces, which go a long way towards eliminating the need for multiple inheritance.
The problem that gets solved in both cases is the idea that you can see a single object in multiple ways (ie talk to it in different ways to the same object).


The last diamond pattern I remember seeing in my work was due to interfaces, and in my experience interfaces encourage multiple inheritance. I feel like I should lay out the situation, because many people are commenting that they've never seen one. I believe this is a very simple case:

This was in a shooting game. The game had base objects that could be in the scene: We'll call the class for those "Actor". Some things to shoot were explicitly targets, so we'll call that class "Target". A target was just a specialized form of actor, so it inherited from Actor. So far, we just have two classes:

Actor
|
Target

These game objects all lived in one component. Other components referenced them through interfaces, which were pretty much one-to-one. So, there was an IActor and ITarget, with the same inheritance to not repeat all the shared functions:

IActor
|
ITarget

Naturally, Actor inherits IActor and Target inherits ITarget, so this is what you get:

IActor
/ \
Actor ITarget
\ /
Target

There you go, a C++ diamond encountered in real life. I'm not saying this is an unsolvable problem, but it was a situation that was left alone because there wasn't any consensus on what would be better.

I think the reason is interfaces, which go a long way towards eliminating the need for multiple inheritance.
The problem that gets solved in both cases is the idea that you can see a single object in multiple ways (ie talk to it in different ways to the same object).


The last diamond pattern I remember seeing in my work was due to interfaces, and in my experience interfaces encourage multiple inheritance. I feel like I should lay out the situation, because many people are commenting that they've never seen one. I believe this is a very simple case:

This was in a shooting game. The game had base objects that could be in the scene: We'll call the class for those "Actor". Some things to shoot were explicitly targets, so we'll call that class "Target". A target was just a specialized form of actor, so it inherited from Actor. So far, we just have two classes:

Actor
|
Target

These game objects all lived in one component. Other components referenced them through interfaces, which were pretty much one-to-one. So, there was an IActor and ITarget, with the same inheritance to not repeat all the shared functions:

IActor
|
ITarget

Naturally, Actor inherits IActor and Target inherits ITarget, so this is what you get:

IActor
/ \
Actor ITarget
\ /
Target

There you go, a C++ diamond encountered in real life. I'm not saying this is an unsolvable problem, but it was a situation that was left alone because there wasn't any consensus on what would be better.

If IActor and Target were pure virtual interfaces, I'm not sure that really counts, since a pure virtual interface will have no implementation. Since the implementation inheritance is solely between Actor and Target, there's no ambiguity as to which class provides the implementation for which functions. Even if at the language level it's technically "multiple inheritance," this scenario isn't what most people refer to when they talk about the diamond problem, because the diamond problem specifically refers to situations where it's ambiguous to the compiler what implementation should be used for a virtual function.

In other words, not all diamonds are diamond problems.

Naturally, Actor inherits IActor and Target inherits ITarget, so this is what you get:
IActor
/ \
Actor ITarget
\ /
Target
There you go, a C++ diamond encountered in real life. I'm not saying this is an unsolvable problem, but it was a situation that was left alone because there wasn't any consensus on what would be better.


Not in C++.

The only way they form a diamond is if the inheritance is marked as virtual.

Also, virtual inheritance is different from virtual functions or abstract classes. So those are just name confusion.

If you used:

class IActor {...}

class Actor : public IActor {...}

class ITarget : public IActor {...}

class Target: public Actor, ITarget {...}

Then you have something like this:

IActor IActor
| |
Actor ITarget
\ /
Target

If you want to create a diamond in C++, you need to specifically state that the inheritance is virtual.

class Actor : public virtual IActor {...}

class ITarget : public virtual IActor {...}

Because that is a very deliberate step, creating a diamond must be a deliberate design decision. If you accidentally share a parent, it will make the parent separate. If you want to createa diamond you must specifically do so, intentionally, on purpose. It doesn't happen in c++ on accident.

"Interfaces" in C++ should use public virtual inheritance.


I've come today to ask you if you know of a programming language that has a good solution for the diamond problem unlike C++'s workarounds of virtual inheritance, using :: to specify which super-class to get the implementation from etc.

Java uses the same exact solution, except they embedded it as a language feature/rule, as opposed to C++'s flexible "do what you want, but please don't be stupid" system.

In C++ an "interface" is just a convention -- if you make an "abstract base class" (something with only unimplemented virtual functions), we call it an interface, and we use virtual inheritance (with multiple virtual inheritance being acceptable). Any other kind of class we call "concrete" by convention, and we use regular inheritance and restrict ourselves to only ever inheriting from one concrete class. Multiple inheritance of concrete classes is possible, but by convention it's a bad thing(tm) and shouldn't be done.

In Java, interface-classes and concrete-classes gained their own keywords, and virtual-inheritance got the implements keyword and concrete-inheritance got the extends keyword. Moreover, the only-ever-inherit-from-one-concrete-class convention became a compiler-enforced rule.

Java just took C++'s conventional solution and locked it down to prevent bad and creative use of it.

C++ also supports implementation-inheritance, without interface-inheritance via the private keyword. AFAIK most other language lack this feature, which isn't even really present in traditional OO design.

As mentioned above though, inheritance is extremely rare in good OOP code (someone tell Java practitioners please!). It's a rule of OOP to use composition where you can and inheritance only where you must (and that inheritance must obey LSP).

I think the most important thing about the diamond problem is to understand the implications, and to understand that there is no diamond problem at all (in fact, the diamond is the solution to the problem).

If you have base class A and classes B and C which derive from it, and class D which derives (non-virtually) from both B and C, then you have a class D with two ancestors which each have a base class (which happens, by sheer coincidence, to be the same).
Both ancestors inherit a virtual function from their respective base class which happens to be the same. By convention, that function should be pure virtual (so both B and C need to define it) but it needs not be. It's perfectly allowable to provide an implementation in A as well.

So, in one word, you have two, possibly three versions of the same function[1], and when you call that function on a D object, it's impossible to tell which one you want to call. You must either say d.A::f(), d.B::f(), or d.C::f(), or you must take the objects address and do a static cast.

This is, however, not a diamond. It's a tree with "a twist and a knot" in its leaves. Or something. But it's not a diamond.

Now what virtual inheritance does is, it turns that knotty tree into a diamond. You now no longer have two base classes which are accidentially on top of each other and from which the two ancestors inherit and possibly override differently a (accidentially identical) virtual function. You now have exactly one base class, and exactly one final overrider in your most derived class (which might call one of the ancestors' implementations, or do something completely different). Problem solved. Thus, the diamond is the cure, not the illness.

Of course, virtual inheritance makes the assumption that you actually want that. This is usually, but not always, the case. You might want something different, too (but then you must tell the compiler what you want every time to make the call unambiguous).



[1] Indeed, they could just as well be entirely differrent, unrelated functions which only accidentially have the same name and function arguments. You cannot know.


Note that extra vtables may be needed for the compiler to transparently do the "magic" so object size and raw pointer addresses are not always "immediately intuitive" (they can indeed be different for the "same" object, but you should not care about raw pointer addresses anyway), and the whole thing doesn't just work with functions, but with data types as well.

Try and figure the output of the following snippet:


#include <stdio.h>

struct A  { int x[100]; };
struct B1 : virtual public A { };
struct C1 : virtual public A { };
struct B2 :         public A { };
struct C2 :         public A { };

struct D :          public B1, public C1 {};
struct E :          public B2, public C2 {};
struct F :  virtual public B2, virtual C2 {};

int main()
{
    printf("sizeof(D) = %llu\n", sizeof(D));
    printf("sizeof(E) = %llu\n", sizeof(E));
    printf("sizeof(F) = %llu\n", sizeof(F));

    return 0;
}

On my machine, output is

sizeof(D) = 416
sizeof(E) = 800
sizeof(F) = 808

The first obviously has 100 elements (and two vtables), the next one has 200 elements (and no vtable), and the last has 200 elements and a vtable.

This topic is closed to new replies.

Advertisement