Difficulty: Intermediate
What is the diamond problem? How do Java, C++, and Python handle it?
The diamond problem is what happens when a class inherits from two classes that both inherit from the same grandparent. Draw it out and you get a diamond: A at the top, B and C in the middle, and D at the bottom inheriting from both B and C. The trouble is ambiguity.
Suppose A has a method hi(), and both B and C override it. When you call hi() on a D object, which version should run: B's or C's? There is also a data problem: D would contain two copies of A's fields, one through B and one through C, so a reference to a field of A is ambiguous. Languages that allow multiple inheritance of classes must define a rule for this.
Java sidesteps the problem for classes by allowing only single inheritance of classes. But interfaces with default methods bring a limited version of the diamond back. If two interfaces provide a default method with the same signature and a class implements both, the compiler forces you to resolve it by overriding the method in the class. Inside that method you can choose explicitly using the InterfaceName.super.method() syntax. A class always wins over interface defaults, and a more specific interface wins over a less specific one, but when B and C are unrelated siblings, you must decide. Because interfaces have no state, there is no duplicate data problem.
C++ allows full multiple inheritance, so it must solve both the method and data ambiguity. Without help, Hybrid inheriting from Dog and Cat, which both inherit from Animal, contains two Animal sub-objects and h.legs is ambiguous, causing a compile error. The fix is virtual inheritance: write struct Dog : virtual Animal and struct Cat : virtual Animal, and now Hybrid shares a single Animal sub-object. The cost is a little extra indirection and a rule that the most derived class initialises the virtual base.
Python supports multiple inheritance and resolves the ambiguity with a deterministic method resolution order, computed by the C3 linearisation algorithm. For D(B, C) the order is D, B, C, A, object. Methods are looked up in that order, and super() follows the same chain, which is why in the example both B and C print, then A, when each calls super().hi(). This cooperative multiple inheritance is powerful but needs everyone in the chain to call super consistently. You can inspect the order using ClassName.__mro__.
The design lesson interviewers want: the diamond problem is one of the reasons Java, C#, and Kotlin restricted multiple inheritance to interfaces. Whenever you feel the need for multiple inheritance, consider composition, mixins that carry no state, or small interfaces instead.
Bonus point: mention that Scala solves a similar problem with traits linearisation, and Kotlin, like Java, forces you to override when two interfaces conflict.
interface A { default void hi() { System.out.println("A"); } }
interface B extends A { default void hi() { System.out.println("B"); } }
interface C extends A { default void hi() { System.out.println("C"); } }
class D implements B, C {
@Override
public void hi() { // mandatory: B and C conflict
B.super.hi();
C.super.hi();
}
}
public class DiamondJava {
public static void main(String[] args) {
new D().hi();
}
}
#include <iostream>
using namespace std;
struct Animal { int legs = 4; };
struct Dog : virtual Animal {};
struct Cat : virtual Animal {};
struct Hybrid : Dog, Cat {};
int main() {
Hybrid h;
h.legs = 3; // unambiguous, only one Animal exists
cout << h.legs << endl;
}
Remove the virtual keyword and h.legs becomes ambiguous and fails to compile.
class A:
def hi(self): print("A")
class B(A):
def hi(self):
print("B"); super().hi()
class C(A):
def hi(self):
print("C"); super().hi()
class D(B, C): pass
D().hi()
print([k.__name__ for k in D.__mro__])
diamond-problem, multiple-inheritance, virtual-inheritance, mro