skip to content

Multiple Inheritance Limits

Why a type may inherit many contracts but usually only one implementation: the diamond problem and the state conflicts it creates. Interviewers want the reason for the restriction, not the rule.

on this pageshow

questions

2

Some languages let a class have two supertypes that share a common ancestor, and others refuse. Explain why inheriting a type, inheriting an implementation, and inheriting state are three separate problems, and show how at least three named languages draw the line differently.

level: middleimportance: must knowfreq 58%

answer

  1. type = obligations, unions freely
  2. implementation = conflict, but decidable
  3. state = share or duplicate, no right default
  4. C++ virtual base vs replicated subobject
  5. Java/C#/Rust: bodies yes, fields never

basics

~20 s

Types are only obligations, so they union freely. Implementation conflicts are decidable — pick one, order them, or error. State is the hard layer: a shared ancestor's fields must be duplicated per path or shared, and neither default is right for every program.

solid answer

~60 s

The three layers fail differently, and languages cut at different depths. - **C++** inherits all three. By default every path gets its **own copy** of the shared ancestor's subobject, so a diamond-shaped class holds two sets of those fields and an unqualified access is ambiguous; marking the base `virtual` collapses them into one, paid for with a runtime offset lookup and with construction of that base moved to the most-derived class. - **Python** also inherits all three, but an instance has a single attribute dictionary, so duplication is not even expressible — there is always exactly one `self.n`. The cost is that unrelated mixins silently share one name namespace. - **Java** allows many supertypes for type and, since Java 8, for implementation via default methods — but never instance fields. Two unrelated defaults are a compile error until the class overrides. - **Rust** traits carry bodies but no per-instance data; two traits may both supply `m`, and the *call site* disambiguates. State is what forces the share-or-duplicate choice, so languages that ban inherited fields dodge the diamond's hard half.

code

cpp · 12 lines
cpp
struct Base { int n = 0; };
struct L : Base {};
struct R : Base {};
struct D : L, R {};

D d;
// d.n = 1;          // error: ambiguous, there are two Base subobjects
d.L::n = 1;
d.R::n = 2;          // two independent fields in one object

// struct L : virtual Base {}; struct R : virtual Base {};
// -> exactly one Base, and D's own constructor initialises it

go deeper

for a junior

Recall the three layers by name and give one example each: a class can promise many types freely, two inherited method bodies can clash, and two inherited copies of the same fields is the genuinely hard case.

for a middle

Be able to draw the diamond and answer "one copy or two?" for C++ (two by default, one with a virtual base), Python (always one, single attribute dictionary), and Java (the question cannot arise, no inherited fields).

for a senior

Explain why no default is universally correct — independent bookkeeping mixins want two copies, identity fields want one — and what banning inherited fields actually buys: it converts an undecidable semantic question into an ordinary API design question.

for a principal

Frame it as a language-design tradeoff: where you cut determines whether reuse conflicts surface at declaration time, compile time, or call site, and argue for composition plus abstract accessors as the design that keeps the sharing decision explicit and reviewable in a large codebase.

## Three things a supertype can hand down When a class names a supertype it can acquire up to three different things, and answers that collapse them into "multiple inheritance is dangerous" miss where the real difficulty actually sits. 1. **Type** — the promise that instances can be used wherever the supertype is expected, plus the set of operation names and signatures that promise entails. Type is a *set of obligations*, and sets union without conflict: being both comparable and serializable demands two things and creates no ambiguity. Practically every language allows unlimited multiple inheritance of type. Even Java, popularly described as having "no multiple inheritance", lets a class implement any number of interfaces. 2. **Implementation** — actual method bodies. Two supertypes may supply a body for the same signature. Now there *is* an ambiguity, but it is decidable: pick one by rule, reject the program, or compute an order. Nothing about the object's memory layout is at stake, so any consistent answer works. 3. **State** — fields. This is where a genuine, unavoidable design choice appears, and it appears specifically in the *diamond*: class D reaches ancestor A through two different paths. Does a D object contain one A-worth of fields or two? ## Why the diamond of state has no default answer Both answers are sometimes correct. If A holds a `counter` and the two intermediate classes are two independent bookkeeping mixins, two copies is exactly what you want — each mixin counts its own thing. If A holds an `id` that identifies the object, two copies is nonsense; the object would have two identities. A compiler cannot infer which case you are in from the declaration alone. So a language must either pick a default and offer an opt-out, ban the diamond outright, or ban inherited fields altogether so the question never arises. ## How named languages land **C++** takes the maximal position and hands the choice to the programmer at *declaration* time. Ordinary (non-virtual) inheritance replicates: with `struct L : Base` and `struct R : Base`, a `struct D : L, R` object contains two `Base` subobjects, and an unqualified `d.n` is ambiguous — you must write `d.L::n` or `d.R::n`, and they are two independent fields. Declaring `struct L : virtual Base` and `struct R : virtual Base` makes the `Base` subobject **shared**, so all paths see one copy. The cost is real, and is why sharing is not the default: a non-virtual base sits at a compile-time-constant offset inside the object, whereas a virtual base's location must be found through an indirection at run time. Responsibility for constructing the shared base also moves to the most-derived class — the intermediate classes' initialisers for it are ignored, which surprises people the first time they hit it. **Python** allows multiple inheritance of everything, yet the replication question cannot arise, because instance state lives in one `__dict__` keyed by attribute name. There is always exactly one `self.n` no matter how many ancestral paths declare it. That is a genuinely different resolution, not a weaker one: Python did not solve the share-or-duplicate problem, it made duplication inexpressible. The price is paid elsewhere — two unrelated mixins that both use `self._cache` collide silently, with no diagnostic at class-creation time and no way to ask for separate storage short of name mangling or composition. **Java** and **C#** cut between implementation and state. A class may inherit any number of interfaces, and since Java 8 (C# 8 for default interface members) those interfaces can carry method bodies — but never instance fields. So implementation conflicts exist and are settled by rule: in Java, if one interface extends the other the more specific default wins; if they are unrelated, it is a compile error and the class must override, optionally delegating with `Loud.super.say()`. No layout question ever arises, because no field was inherited. **Rust** goes one step further. A trait carries method bodies, associated types and consts, but no per-instance data whatsoever. Two traits may each provide `m` for the same type, and that is not an error at all — it becomes an error only at a call site that cannot tell which is meant, and is resolved there with fully-qualified syntax. Storage a trait needs is requested as a required accessor method the implementor supplies, which turns the sharing question into an explicit part of the implementor's API rather than a layout decision. ## The transferable conclusion Banning inherited fields is the cheap way out of the diamond: it converts a semantic problem the compiler cannot solve (one copy or two?) into an API problem the programmer already knows how to solve (declare an accessor, or hold a component and delegate). That is why blanket condemnation of multiple inheritance is too coarse — the danger is concentrated in the state layer, and a language that permits multiple inheritance of bodies without fields has taken almost none of the risk.

  • Why does C++ require the `virtual` keyword on the base instead of simply making sharing the default?
    Because sharing is not always what you want, and it is not free. A non-virtual base sits at a compile-time-constant offset inside the derived object, so member access is a fixed address computation; a virtual base must be located through an indirection at run time. Sharing also changes construction semantics — the most-derived class becomes responsible for initialising the virtual base, and intermediate classes' initialisers for it are ignored. C++ defaults to the cheaper, more local behaviour and makes you opt in to sharing.
  • If a language bans inherited fields, how do you express "these two mixins both need a counter"?
    You make the storage part of the contract instead of part of the layout: the trait or interface declares an abstract accessor (say `count()` / `setCount()`) that the implementing class must provide. The implementor then decides explicitly whether the two mixins get one field or two, which is precisely the decision the compiler could not make for you. The alternative is composition — hold each mixin as a member object with its own state and forward the calls.
  • Java forbids inherited fields but interfaces can declare constants. Doesn't that reintroduce the diamond?
    No, because interface fields are implicitly `public static final` — they belong to the interface, not to any instance, so there is nothing to duplicate per object and no layout question. You can still get a *name* ambiguity if two inherited interfaces declare a constant with the same simple name, but that is resolved by qualifying it with the interface name, exactly like the default-method case.

Type is a job description, implementation is a trained employee, state is the desk they sit at. Two job descriptions merge fine, two employees can be told who leads, but two desks cannot occupy one square metre — somebody has to decide whether they share it.

saying these in an interview costs you the question

  • Saying "Java has no multiple inheritance" without qualifying it — a Java class may implement any number of interfaces, and since Java 8 those may carry bodies.
  • Treating the diamond problem as purely a method-resolution problem; the resolvable half is method bodies, the hard half is whether the shared ancestor's fields are duplicated or shared.
  • Claiming C++ virtual bases are free or are simply the better choice — they cost a runtime indirection and move construction of the base to the most-derived class.
  • Asserting Python "solves" the diamond with its method order; the resolution order settles which body runs, while the state question disappears only because instances have a single attribute dictionary.
  • Saying multiple inheritance of interfaces with default bodies is as risky as C++ multiple inheritance — without inherited fields, the layout risk is gone entirely.

context

open as a page

When two inherited paths supply a body for the same operation, some languages compute a total order over all ancestors and silently pick a winner, while others refuse to compile until the programmer names one. Compare the two resolution schemes and say what each one costs.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Computed orders (Python's C3, Scala's trait linearization) always produce an answer and let a call chain cooperatively through ancestors the author never named - at the cost of non-local reasoning. Explicit disambiguation (C++, Java, Rust) never surprises you but cannot express stacking and adds boilerplate.

open as a page