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

user3840170
  • 215
  • 1
  • 9
wcminipgasker2023
  • 721
  • 1
  • 3
  • 10
  • 12
    AddToList, DeleteFromListByIndex? – jmoreno Aug 14 '23 at 11:30
  • 57
    Sequentially? In what order? – Joe Sewell Aug 14 '23 at 15:56
  • Does this answer your question? [Is there any "real" reason multiple inheritance is hated?](https://softwareengineering.stackexchange.com/questions/218458/is-there-any-real-reason-multiple-inheritance-is-hated) – Telastyn Aug 14 '23 at 17:16
  • 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 '23 at 18:16
  • 1
    some languages have decided to support it (like python). C++ has decided it would be best not to support it. are you asking if there is documentation of this choice? – njzk2 Aug 14 '23 at 18:49
  • 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. – Silvio Mayolo Aug 14 '23 at 20:03
  • 1
    @SilvioMayolo As per JimmyJames's answer, it's a question any OO language that lets you inherit from two classes might have; if class A has a draw method and class B has a draw method, which one does class C (inherits A, inherits B) call when this.draw is invoked. It's not a theoretical problem, but a practical problem. – prosfilaes Aug 14 '23 at 22:42
  • 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 '23 at 23:58
  • 8
    "Why the result isn't naturally just "d->f() means calls both B.f() and C.f() sequentially"? " - **why should doing this make any sense**? Conceptually, `f` does one specific thing, once (in different ways according to the type). If `B.f` and `C.f` are called sequentially, it will happen twice, yeah? – Karl Knechtel Aug 15 '23 at 08:07
  • Re: "why multiple inheritance is bad" -- [it's not bad; it's just used that way](https://getyarn.io/yarn-clip/aa1c25ef-af97-4a0e-b42d-a43139a70496). – Pete Becker Aug 15 '23 at 12:46
  • 1
    OMG you just crammed three questions into the the title of a Stack Exchange "Question". I guess we're supposed to answer them sequentially? Would we put all three answers in one "Answer", or should we give them each their own Answer? A solution exists (re-engineer StackExchange to handle this case) so I hope that's not a problem. :) – Travis Wilson Aug 15 '23 at 17:08
  • Don't have an answer to the second and third questions (hence not providing a full answer), but the reason it's a problem is simply historical precedent. Long story short, C++ started out as a superset of C, a language that added new features and could be "compiled" into valid C code. Early compilers both tried to adhere to C design standards and were really simplistic, so they wouldn't write code for you, and didn't allow name ambiguity. If you supplied `D::f()`, the compiler knew you wanted that one... but if you didn't, it didn't know which `f()` is `D::f()`,... – Justin Time - Reinstate Monica Aug 15 '23 at 17:39
  • ...wasn't smart enough to choose for you (even modern compilers can't read minds, let alone ones from the '80s), and wouldn't (and couldn't) write a function for you that would call both, so the easiest solution was just to get your attention so you could figure out how to handle it. And a lot of C++'s features and rules were defined by this specific convert-C++-to-C approach, so it makes sense that old compilers causing the diamond problem, combined with "don't pay for what you don't use" and the inability to read your mind, lead to "the compiler won't write a glue function for you". – Justin Time - Reinstate Monica Aug 15 '23 at 17:45
  • Another important thing is, when using these classes polymorphically, just calling the one virtual method in *single* inheritance can break LSP and result in surprising behavior - let alone calling two in sequence. – Filip Milovanović Aug 15 '23 at 19:19
  • 1
    On a semantic level, the answer is simple: a problem is still a problem even if it has solutions. (And that's without getting into the cost of the solution, which the answers are very nicely doing.) – biziclop Aug 15 '23 at 22:10
  • @JoeSewell - Yes. Though just to play devil's advocate, presumably "they get called in declaration order; it's up to the programmer to choose" is workable and without ambiguity. As in, declare the child class with `public B,public C` and the compiler knows to run `B::f()` before `C::f()`. Declare `public C,public B` instead if you want the opposite. Or override `f()` if you want something custom. Granted it's bound to cause some unexpected slip-ups, but what's one more gotcha in a language that's already loaded with gotchas? – aroth Aug 16 '23 at 05:05
  • @aroth - but what if there's a second function, `e` where you want the opposite call order? Solving a function-level "problem" at the class-level seems a bad move. – Damien_The_Unbeliever Aug 16 '23 at 07:39

8 Answers8

81

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
  • 10
Martin Gleich
  • 764
  • 3
  • 6
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.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
  • 10
    Mmmm. A matching method _name_ doesn't necessarily mean matching _meaning/intent/semantics_. – gidds Aug 14 '23 at 14:19
  • 3
    I disagree that the classes are "unrelated". While they could conceivably be, it rarely makes sense. Multiple inheritance is often used for situations where each base class implements a part of the whole, like mixin architectures. – Barmar Aug 14 '23 at 14:49
  • 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. – Austin Hemmelgarn Aug 14 '23 at 14:51
  • 5
    @AustinHemmelgarn especially if there is a framework that defines `Object` as an ancestor of (almost) everything – Caleth Aug 14 '23 at 15:05
  • 3
    @AustinHemmelgarn Good point. Although this problem is more due to using ambiguous English to name things -- `drawWeapon()` and `drawPicture()` shouldn't both be called `draw()` if you anticipate having artists who are also gunfighters. – Barmar Aug 14 '23 at 15:50
  • 2
    There *being* a diamond problem dictates the classes are related. – davolfman Aug 14 '23 at 16:45
  • 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 '23 at 17:24
  • @Ángel But if you're going to have a system where you can combine gunfighters and artists, you clearly need to have distinct method names. "draw()" is ambiguous. The contract of "draw" needs to be specified by the common superclass, and it can't include both shooting a gun and making a pcture. – Barmar Aug 14 '23 at 17:31
  • @Barmar Suppose you got an Artist SuperClass, with subclasses ToddlerArtist and ComicArtist. Both of these make a picture when you call "draw()", but the outcome would probably be very different for both of them. Hell, I could easily see a ToddlerComicArtist which inherits from both and needs to do something entirely dfferent when invoking draw(). – Nzall Aug 14 '23 at 19:28
  • @Nzall IN that case you probably need to override the method in `ToddlerComicArtist`, because the inherited methods do conflicting things. This is different from Gunfighter vs Artist, where the contracts aren't even similar – Barmar Aug 14 '23 at 19:31
  • +1 for the first sentence, although I don't think the example is very good. Why not just a simple `print` method? A, B and C have print methods that do more or less the same thing, printing out the object - but you don't want D to call the print methods of both B and C because then it would print itself twice. – N. Virgo Aug 15 '23 at 02:34
  • 1
    @Barmar What if you want to inherit from both `Artist` from one library and `Gunfighter` from another library, with neither library knowing of the other? – user3840170 Aug 15 '23 at 09:00
  • @user3840170 Then you're in trouble, because the resulting class won't satisfy LSP for at least one of the base classes. That's why you can't do MI haphazardly like this. – Barmar Aug 15 '23 at 14:29
  • Angel, the artist's "draw" method is called, and you get a nice picture. The gunfighter's "draw" method is called, and you are dead. – gnasher729 Aug 15 '23 at 15:14
  • 1
    @Barmar That's exactly the "Diamond problem". Which cannot be solved by ignoring problems. – gnasher729 Aug 15 '23 at 15:18
  • 1
    @gnasher729 I disagree. When people create diamond structures like this, they're working with classes that are intended to be related. `A` is often an ABC that both B and C are subclasses of. – Barmar Aug 15 '23 at 15:23
  • 3
    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 '23 at 17:28
  • one can find examples of reasonable diamond patterns. It can happen when inheritance trees covering different concepts meet, like living being -> male -> human and living being -> animal -> human. I see it as a consequence of choosing a characterization of the problem space. As such, I find your example too simplistic, and no alternative presented. How would this be solvable *without* multiple inheritance? Also, why would it *not* make sense for some method on this to both be executed in such a case? – bytepusher Aug 15 '23 at 18:56
  • @bytepusher: Languages/frameworks like Java/JVM and C#/.NET can support a form of multiple inheritance which allows diamonds, without diamond problems, by splitting the internal aspects of inheritance (the ability of a class to access parts of the parent implementations) and external aspects of inheritance (the ability of a base-class reference to identify a derived-class object). The latter is more broadly useful, but only the former has the diamond problem. – supercat Aug 16 '23 at 19:48
  • 1
    It's certainly not always the case that the inherited methods have the same name only by coincidence. More usually, they have the same name because they perform the same function. – Michael Kay Aug 16 '23 at 20:46
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
  • 10,519
  • 2
  • 23
  • 35
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.
  • 14,567
  • 4
  • 44
  • 65
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
  • 74,672
  • 10
  • 115
  • 187
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
  • 24,682
  • 2
  • 50
  • 92
  • 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. – Toby Speight Aug 15 '23 at 06: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 '23 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. – Toby Speight Aug 16 '23 at 06:43
  • @TobySpeight Got it. Better? – JimmyJames Aug 16 '23 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). – Toby Speight Aug 16 '23 at 14:58
  • @TobySpeight Thanks for the recommendation. I think it's clearer for most readers. – JimmyJames Aug 16 '23 at 15:04
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
  • 3
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.

Travis Wilson
  • 445
  • 3
  • 3