18

I know there have been many post about diamond problem, one of it: Why do you reduce multiple inheritance to the diamond problem?. But I'm not asking what it is or what is the solution of the problem. Also I'm not asking why multiple inheritance is bad. What I don't understand is, why is it a "problem"? Consider:

struct A {};
struct B : A {
  virtual void f() {}
};
struct C : A {
  virtual void f() {}
};
struct D : B, C {};

I know the code about can't compile, as "diamond problem" said, D doesn't know which version of f() (B? C?) should call. What I don't understand is, what assumptions/rules apply at here so that D is expected to select either one method only? Why the result isn't naturally just "d->f() means calls both B.f() and C.f() sequentially"? Why must D require to select either one?

Another scenario:

struct A {
  virtual void f() {}
};
struct B : A {};
struct C : A {};
struct D : B, C {};

I know it would not compile. But what I don't understand is, why isn't

d->f();

just naturally "D call A's f()" once? What is the "ambiguous" thing here? Why must I assume d->f() would logically call A's f() twice at here?

Note: I'm not asking why is multiple inheritance is bad like this: Is there any "real" reason multiple inheritance is hated?

While multiple inheritance has some problems, the problems seems do have solutions: e.g.: if both B and C has a class member name, a language designer can design some syntax to excess like this: d->B::name or d->C::name. And I believe programmers have their rights to choose or refuse using multiple inheritance. What I don't understand is, why does a "problem" having solutions called a "problem"?

8 Answers8

83

One problem with your approach is that it only works if the method f() has no return value. What should happen if it returns some value? Should it only return the second one?

With variables you have the same problem. Look at the following code:

class A
{
    int Field;
};

class B : public A {};

class C : public A {};

class D : public B, public C {};

Do you expect D to contain a single int Field or two? You can of course also solve this problem. Maybe even add a new keyword to allow the programmer to make the choice.

But more generally I would say the issue with the diamond-problem isn't that you cannot find a solution. But that diamond inheritance automatically leads to more complex programs. All solutions you can find are kind of arbitrary. You could also do a depth-first-search on the inheritance tree and pick the first method you find. Or do a breadth-first-search. Or perform C3 linearization. And so on...

You have to make your language more complex, handle more edgecases, add more keywords. You can check out the Wikipedia article on the diamond problem to see how various language handle these issue.

In my opinion there are quite a few options and the solutions get quite complicated. So many languages just avoid the problem completely by not allowing multiple-inheritance at at. Which is often a good solution in itself.

For many language the goal isn't to allow execution of any arbitrary piece of code and just do something. But to only allow programs that are easily understandable for developers and maintainers.

Cave Johnson
  • 323
  • 2
  • 11
38

It is very, very, very unlikely that I want to call both methods.

Imagine I inherit from three classes, Gunfighter, Chessgame, and Artist, and each has a Draw() method. The best outcome would be changing the method names. The second best is the compiler telling me that it can't choose the method. The absolute worst would be calling three methods.

Remember that multiple inheritance is inheriting from two or more totally unrelated classes. If methods have the same name, that is pure coincidence.

Edit: Some people claim that being used in multiple inheritance makes some classes unrelated. Well, absolutely not! It means each has features - completely separate features - that a subclass finds desirable. And these features are not related, otherwise you get an absolute mess.

gnasher729
  • 49,096
16

Your proposed solution only works for void functions, and only when the side-effects of them can safely compose. You still have to choose which value to return.

class A{
};
class B : public A{
public:
  virtual int f(){ return 1; }
};
class C : public A{
public:
  virtual int f(){ return 2; }
};

In your second example, there are two A subobjects in each D, it is still ambiguous which instance of A to bind to this in f

Caleth
  • 12,190
12

What is the "ambiguous" thing here? Why must I assume d->f() would logically call A's f() twice at here?

Your question is actually just a reword of why is diamond inheritance a problem, and the answer is simple: because there are multiple competing "solutions", and neither is better than the others.

That is, whether B::f and C::f should both be called, or only one, or neither, will ultimately depend on the usecase at hand:

  1. If we are talking types of bank accounts, then adding/removing money should most likely only be done once.
  2. If we are talking drawing upon the screen, then drawing both in the same place is obviously wrong.

Hence why diamond inheritance is a thorn in the side: there's no good single way of handling it, depending on the usecase the user will need to be able to choose which way to use.

There are deeper problems -- related to state management in A -- but even with pure interfaces it's already a struggle.

Matthieu M.
  • 15,214
7

Is it a problem?

The problem with the diamond and multiple inheritance is the ambiguity. Not in the syntax, but in the semantics. You just have to decide what you want.

In your code, there are two A subobjects: one A instance for the base of B, and one A instance for the base of C. Each of these A instances is independent, and when you call d.f() it is not clear which one you mean (e.g. if there are side effects, should it be on B's or on C's A?).

But if you think there should be only one A subobject, then you must tell it. In C++ this is done with virtual inheritance, and the following code compiles very well:

class B : virtual public A{};
class C : virtual public A{};
class D : public B,public C{};
...
d.f();
...

So why is it called a problem?

In this latter case, each of the inheritance branch can decide to override f(). If only one does, it's ok, because there is only one A subobject. But if both override f(), there is anew an ambiguity and you will have to tell what you really want by explicitly implementing D::f().

The additional case:

class B : virtual public A{
public:
  void f() override {cout<<"It's B"<<endl;}
};
class C : virtual public A{ 
public:
  void f() override {cout<<"It's C"<<endl;}
};
class D : public B,public C{
public:
  void f() override { // looks a lot like forwarding when preferring composition over inheritance
    cout<<"It's D, this means:"<<endl;
    B::f(); cout << "and at the same time "; C::f();
  }
};

So "diamond problem" just reminds that it won't be easy. The real problem is not that there is a diamond, but that you might find a diamond every time you multiply inherit.

So instead of just specialising a class without worrying about its ancestors, you need on contrary to know all the details of the inheritance web, which undermines somehow the ease of use of inheritance.

P.S: The diamond can occur without multiple inheritance, with interfaces and default methods

Christophe
  • 81,699
5

The diamond problem is a problem because there is no one obvious answer to what is resolved. That is, your solution might work, but you can't assume that everyone else will think that's the obvious (or desirable) approach.

The evolution of Java is an interesting case around this topic. In the original design of Java, you could inherit abstract method declarations from many interfaces but only inherent concrete method definitions from one class. This eliminated the 'diamond problem'.

Later on, however, the ability to add concrete methods to interfaces was introduced. The diamond problem was potentially back in play. In order to deal with it, the language specification has very clear rules about what method will be used when there are two different concrete definitions available for a method. Here's an article that explains those rules and I think understanding the choices that were made for Java provide a good understanding of the challenges and trade-offs involved.

In short, multiple inheritance can be unambiguous if there's an unambiguous specification of how names are resolved in diamond situations.

JimmyJames supports Canada
  • 30,578
  • 3
  • 59
  • 108
1

In both of your code examples, there are two instances of A in each instance of D. The inheritance hierarchy is

A   A
↑   ↑
B   C
 ↖ ↗
  D

In order to make it diamond-shaped, B and C must inherit virtually from A.

Your second example doesn't compile because there is an ambiguity in which copy of A should be passed to f as its this parameter. If you make the inheritance virtual, so that there is only one copy of A in d, then it will compile.

Your problem is really just a problem of multiple inheritance. This has been covered adequately in other answers.

benrg
  • 111
0

Why the result isn't naturally just "d->f() means calls both B.f() and C.f() sequentially"?

Because almost every procedural programming language already has a way to express that:

void f() {
  B.f();
  C.f();
}

This is an example of composition. In this case, being explicit about what you want is just as concise as the less-commonly-understood code you propose.

If there are a dozen methods like f(), and you want to apply the same argument to all of them, then you could start to argue that multiple-inheritance-via-sequential-calls is more concise. But you also dramatically reduce the number of real-life applications, and you dramatically increase the odds of unexpected error.