December Code

Diamond problem interview questions, with answers

The diamond problem is what happens when a class inherits from two classes that share a base: the class diagram forms a diamond, and the language has to decide how many copies of the shared base the bottom class gets and which version of a shared method it runs. Every language that allows multiple inheritance answers those two questions differently, and interviewers use that to check whether you know the language you claim.

Below are the questions asked about it, with the C++, Python and Java answers side by side. The inheritance page introduces the problem and the interface page covers how Java limits it; this page is about how each language resolves it. Then take the free OOP diagnostic — ten questions drawn from all fifteen OOP topics — to see which concepts you can define but not apply.

The questions, with answers

  1. 1.What are the two separate things that go wrong in the diamond problem?

    In short: Duplicated state — the bottom class may get two copies of the shared base's fields — and ambiguous behaviour — one call may reach two different versions of the same method.

    Take a Device base class with a serial number, Printer and Scanner both inheriting from it, and Copier inheriting from both. Without special handling a Copier contains two Device parts, one through Printer and one through Scanner, so it has two serial numbers that can disagree. Separately, if Printer and Scanner both override a describe method from Device, a call to describe on a Copier has two equally good candidates. Languages treat the two halves differently. Java avoids the state half entirely, since a class never inherits fields from more than one class, and handles the behaviour half with rules for default methods. C++ lets you choose one shared copy with virtual inheritance. Python always shares one copy and orders the candidates with its method resolution order.

  2. 2.How does virtual inheritance solve the diamond problem in C++?

    In short: Declaring the intermediate bases virtual, as in class Printer : virtual public Device, makes every path share one Device subobject, so the bottom class has one copy of its fields.

    Without virtual, a Copier holds two Device subobjects and an unqualified use of serial does not compile, because the compiler cannot tell which one is meant. With virtual inheritance on both Printer and Scanner, the two paths lead to the same single Device, so c.serial is unambiguous and there is only one serial number to keep consistent. The keyword goes on the edges into the shared base — on Printer and Scanner — not on Copier. It is not free: a virtual base is reached through an extra level of indirection, and the most-derived class takes over the job of constructing the shared base, which is the next question.

    class Device  { public: int serial = 0; };
    class Printer : virtual public Device { };
    class Scanner : virtual public Device { };
    class Copier  : public Printer, public Scanner { };
    
    int main() {
        Copier c;
        c.serial = 42;   // one shared Device: unambiguous
        // without the two 'virtual' keywords this line does not compile: 'serial' is ambiguous
    }
  3. 3.Who constructs the shared base under virtual inheritance, and in what order?

    In short: The most-derived class constructs the virtual base, directly and first; the calls Printer and Scanner make to Device's constructor are ignored when a Copier is built.

    Since Printer and Scanner share one Device, letting each of them construct it would be ambiguous, so C++ gives the job to the class actually being created. For a Copier the order is the virtual base Device first, then the direct bases in the order the class declaration lists them — Printer, then Scanner — and finally Copier's own body. If Device has no default constructor, Copier's constructor must name it in its initialiser list even though Copier does not inherit from Device directly, and forgetting this is the compile error people meet in practice. When a Printer is created on its own, Printer's initialiser for Device is used as normal; it is ignored only when Printer is part of a larger object that shares the base.

    class Device {
    public:
        Device(int s) : serial(s) {}
        int serial;
    };
    class Printer : virtual public Device { public: Printer() : Device(1) {} };
    class Scanner : virtual public Device { public: Scanner() : Device(2) {} };
    class Copier  : public Printer, public Scanner {
    public:
        Copier() : Device(3) {}   // required: Copier constructs the shared Device itself
    };
    // Copier().serial is 3: the Device(1) and Device(2) initialisers are ignored
  4. 4.How do you resolve an ambiguous member without virtual inheritance?

    In short: Name the path: qualify the member with the intermediate class, as in c.Printer::describe(), or override the method in the bottom class and delegate to the version you want.

    Without virtual inheritance there really are two Device parts, and C++ makes you say which one you mean. c.Printer::serial and c.Scanner::serial are two different integers, so setting one leaves the other unchanged — the duplicated-state bug in a single line. For behaviour, the cleanest fix is to override describe in Copier and call Printer::describe(), Scanner::describe(), or both, from inside it; a using-declaration such as using Printer::describe; selects one version for name lookup instead. Qualification works, but a design that needs it at every call site is usually one where composition — a Copier that holds a Printer and a Scanner — would have been simpler.

  5. 5.How does Python resolve the diamond problem?

    In short: Python always keeps one shared base and puts every class into a method resolution order computed by C3 linearisation; super() follows that order, so each class runs once.

    For class Copier(Printer, Scanner), with both inheriting from Device, the method resolution order is Copier, Printer, Scanner, Device, object: children before parents, and parents in the order they are listed. A method is looked up along that list and the first match wins. The important consequence is for super(): inside Printer, super() does not mean Device, it means the next class in the order of the actual object — Scanner, when the object is a Copier. That is what makes the cooperative pattern below work, where every __init__ calls super().__init__() and Device's __init__ still runs exactly once. Calling Device.__init__(self) directly from both Printer and Scanner would run it twice.

    class Device:
        def __init__(self):
            print("Device")
    
    class Printer(Device):
        def __init__(self):
            print("Printer")
            super().__init__()
    
    class Scanner(Device):
        def __init__(self):
            print("Scanner")
            super().__init__()
    
    class Copier(Printer, Scanner):
        def __init__(self):
            print("Copier")
            super().__init__()
    
    print([c.__name__ for c in Copier.__mro__])
    Copier()   # prints Copier, Printer, Scanner, Device: each once
  6. 6.When does Python refuse to create a class because of an inconsistent MRO?

    In short: When two parents demand opposite orders for the same classes — one puts A before B, the other B before A — C3 cannot satisfy both and raises TypeError at class creation.

    Suppose class X(A, B) and class Y(B, A), and then class Z(X, Y). X requires A before B, Y requires B before A, and Z must respect both, which is impossible, so Python stops at the class statement with TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B. The error is useful rather than annoying: a language that silently picked one order would make the meaning of a method call depend on an accident of declaration. In an interview, this is the quickest way to show you know the order is computed rather than simply depth-first: depth-first order never fails, and it visits a shared base before one of its subclasses.

  7. 7.What are Java's rules when a default method is inherited along more than one path?

    In short: Three rules, in order: a method from a class beats any interface default; a more specific interface beats the one it extends; anything still ambiguous must be overridden by the class.

    Rule one: if a superclass declares the method, its version wins over defaults from any interface. Rule two: if interface Printer extends interface Device and both provide a default, Printer's wins because it is more specific — this is the Java diamond made of interfaces, and it resolves without any code from you, as the example below shows. Rule three: if two unrelated interfaces provide the same default and neither rule applies, the class does not compile until it overrides the method, and inside the override it can pick a version with Printer.super.describe(); that is the case the diagnostic question on this page tests. State never enters the problem, because interfaces cannot have instance fields.

    interface Device  { default String describe() { return "device"; } }
    interface Printer extends Device { default String describe() { return "printer"; } }
    interface Scanner extends Device { }             // inherits Device's default
    
    class Copier implements Printer, Scanner { }     // compiles: Printer is more specific
    
    // new Copier().describe() returns "printer"
  8. 8.Should you use multiple inheritance at all, and what are the alternatives?

    In short: Use it to combine interfaces or stateless mixins; to combine state, prefer composition — hold the parts as fields — which never produces a diamond.

    The diamond's trouble is almost entirely about shared state. Inheriting several pure interfaces — or, in C++, several abstract classes with no data members — is safe and common, because there is nothing to duplicate and every method must be implemented once in the bottom class. Python mixins, small classes that add one behaviour and cooperate through super(), are the same idea. When the parents carry state, composition is usually clearer: a Copier that owns a Printer and a Scanner and forwards calls to them has no ambiguity, no special constructor rules, and parts that can be replaced independently. 'Favour composition over inheritance' is the maxim interviewers expect you to quote here, followed by the reason.

How the diagnostic asks it

One question from the OOP bank, exactly as a sitting would show it. The bank has 3 on multiple inheritance & diamond problem and 60 across OOP.

Multiple Inheritance & Diamond Problem · mediumOOP-047

In Java 8 or later:

interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B { }

What must be true for this to compile?

  1. 1C must override hello() itself; inside it, C may delegate with A.super.hello() or B.super.hello()correct
  2. 2Nothing — the compiler merges the two default methods
  3. 3It can never compile; a class cannot implement two interfaces that share a method name
  4. 4Nothing — Java uses A's version because A is listed first

When a class inherits the same default method from two unrelated interfaces, Java reports a compile error unless the class resolves the conflict by overriding the method; the override can pick one with the InterfaceName.super.method() syntax. Declaration order carries no meaning, methods are never merged, and implementing two interfaces with a common method name is fine as long as the class provides its own implementation. This is Java's answer to the diamond problem for behaviour (state is never inherited from interfaces).

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 OOP questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated