17

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"?

17
  • 12
    AddToList, DeleteFromListByIndex?
    – jmoreno
    Aug 14, 2023 at 11:30
  • 57
    Sequentially? In what order?
    – Joe Sewell
    Aug 14, 2023 at 15:56
  • 30
    So you want a printer driver that inherit from both the HewlettPackard class and the PostScript class to print twice because both parent class implement their own print function?
    – slebetman
    Aug 14, 2023 at 18:16
  • 17
    To be clear, diamond inheritance as you phrase it is a C++-specific problem. C++ decided to disallow this and throw up an error. Other languages made other choices, but there's no theoretical or mathematical limitation preventing such a class hierarchy from existing. Lots of bad programming books talk about the diamond problem as this big scary universal computer science problem that there's no way around, but it's really an incidental detail of C++ specifically. Aug 14, 2023 at 20:03
  • 19
    The core assertion that this question is rooted on is that it's better to do something even if it only works well some of the time; as opposed to not doing anything. I very much disagree there. Patchwork implementations are shoddy and lead to developers needing to know esoteric tricks and behaviors of the language that are not logically consistent or obvious. You're suffering from the bias that your first thought seems obvious to you because it's your first thought, but it's not going to be everyone's first thought. You've also not considered edge cases, e.g return values and error handling.
    – Flater
    Aug 14, 2023 at 23:58

8 Answers 8

82

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.

0
37

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.

20
  • 10
    Mmmm. A matching method name doesn't necessarily mean matching meaning/intent/semantics.
    – gidds
    Aug 14, 2023 at 14:19
  • 11
    @Barmar The problem is that the language runtime has no way to know for certain that the classes are, in fact, related beyond happening to share come common ancestor. In this example, Gunfighter and Artist are both logical descendants of some theoretical Person class, but that does not mean that their draw() method would do the same thing. Aug 14, 2023 at 14:51
  • 5
    @AustinHemmelgarn especially if there is a framework that defines Object as an ancestor of (almost) everything
    – Caleth
    Aug 14, 2023 at 15:05
  • 4
    @Barmar if I'm drawing a room with a bunch of objects, it is actually better to be able to call draw() on each of them, rather than needing a big switch to select if I shall call drawWeapon(), drawPicture(), drawDog() or drawManWithBeardAndBlueEyes(). That's precisely the point of why to inherit and follow a given interface.
    – Ángel
    Aug 14, 2023 at 17:24
  • 4
    The diamond problem isn't with scenarios where parent classes define unrelated functions with the same name, but rather with scenarios where both derived classes override the same method from a parent class. Suppose a parent class defines draw() which draws a string in Times New Roman 12, one child overrides it to use Helvetica, one overrides it to use Courier, and some other class derives from both. If a pointer to an instance of the latter class is passed to a function expecting to receive a base-class pointer, and that function calls draw, how should it draw the string?
    – supercat
    Aug 15, 2023 at 17:28
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

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.

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

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.

6
  • Given that this is a c++ question, I think we'd say that Java inherits declarations from the base interfaces, but only one definition from the single base class. Aug 15, 2023 at 6:48
  • @TobySpeight That's how it was before Java 8 but in current versions, you can define implementations on interfaces (with limitations) as well as classes. Let me know if I missed your point.
    – JimmyJames
    Aug 15, 2023 at 15:38
  • I'm talking about your second paragraph, and translating it to C++ terminology to match the question. Thanks for the information about Java 8 - I stopped doing Java around JDK 1.4 or 1.5, so definitely no longer up to date in that environment and in learning mode by paragraph 3. Aug 16, 2023 at 6:43
  • @TobySpeight Got it. Better?
    – JimmyJames
    Aug 16, 2023 at 14:49
  • 1
    Yes, I think that's clearer to someone coming from C++. (You already had an upvote from me, so can't give you another). Aug 16, 2023 at 14:58
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.

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.

Not the answer you're looking for? Browse other questions tagged or ask your own question.