Inheriting an implementation and being usable wherever another type is expected are two different relationships. Which languages let you have one without the other, and what is each mechanism actually for?
answer
- subtyping = substitutable at use sites; subclassing = where the code came from
- C++ public = is-a, private = implemented-in-terms-of
- Go: embedding for reuse, structural interfaces for subtyping, unrelated
- Rust: no implementation inheritance; default trait methods carry the reuse
- Java `extends` grants both, so reuse leaks a promise
basics
~20 sSubclassing is code reuse; subtyping is substitutability. C++ private inheritance and Eiffel non-conforming inheritance give reuse with no substitutability; Go embedding and Rust default trait methods give reuse without inheritance; Go and TypeScript grant subtyping with no declaration at all.
solid answer
~50 sThey are separate relations that Java and C# happen to weld together in one keyword. - **C++** separates them in the language: `class Stack : private Deque` inherits every member but creates no implicit conversion to `Deque&`, so the compiler enforces "implemented in terms of" while denying "is-a". Only public inheritance is subtyping. - **Eiffel** does the same with non-conforming inheritance (`inherit {NONE}`) -- explicit reuse that carries no conformance claim. - **Go** has no inheritance at all: struct embedding forwards methods (reuse), interfaces are satisfied structurally (subtyping), and embedding a type never makes the outer type usable where the embedded one is expected. - **Rust** has no implementation inheritance either; traits supply polymorphism, and default method bodies supply the reuse that a base class would have. - **Java/C#**: `extends` grants both at once, so inheriting for reuse silently publishes a substitutability promise you never meant to make.
code
cpp · 9 linesclass Deque { public: void push_front(int); void push_back(int); };
class Stack : private Deque { // subclassing, NOT subtyping
public:
void push(int x) { push_back(x); } // reuse of the inherited member
};
void consume(Deque& d);
// consume(s); // rejected: no implicit conversion from Stack to Dequego deeper
State both relations in plain words -- substitutable at use sites versus where the code came from -- and give interface implementation as the everyday example of subtyping without shared code.
Name a language that separates them by keyword and one that has no implementation inheritance at all, and explain what each mechanism is for.
Discuss the consequence for published APIs: in a welded language every base you extend becomes part of your contract, and reuse-only relationships need delegation.
Frame it as a boundary-design decision -- which relations your platform's public types are allowed to assert, and whether the language's default forces you to over-promise.
## Two relations, often confused because one keyword grants both **Subtyping** is a statement about *use sites*: wherever a value of type B is expected, a value of type A may appear. It is about the interface a caller sees and the behaviour it may rely on. **Subclassing** is a statement about *construction*: this class obtains members -- fields, method bodies, sometimes constructors -- from another class. It is a code-sharing mechanism. They are logically independent. A type can be a subtype of another with no shared code (two unrelated classes implementing the same interface). A type can share code with another and not be a subtype (a stack built out of a deque's internals). The reason candidates confuse them is that `extends` in Java, C# and many others grants both in one stroke, so the only spelling for reuse also publishes a substitutability claim. ## Languages that separate them explicitly **C++** is the clearest case, because the separation is a keyword. `class Stack : public Deque` is subtyping plus subclassing: a `Stack&` converts to `Deque&`. `class Stack : private Deque` is subclassing only: members are inherited and usable inside `Stack`, but no implicit conversion exists, so no caller can substitute. The C++ community's own guidance is exactly this reading -- public inheritance means is-a, private inheritance means implemented-in-terms-of, and the latter is a slightly more powerful form of composition (it permits overriding a virtual and accessing protected members) at the cost of tighter coupling. **Eiffel** offers non-conforming inheritance: `inherit {NONE} SOME_CLASS` reuses without conformance. That Eiffel -- a language that treats inheritance as its universal structuring tool -- needed such a feature is itself the argument that the relations are distinct. **Go** removes implementation inheritance entirely and then supplies both jobs separately. Struct embedding promotes the embedded type's methods onto the outer type, which is reuse by delegation with syntax support. Interface satisfaction is structural and undeclared: a type satisfies an interface by having the methods. Critically, embedding `Reader` into `MyThing` does *not* make `MyThing` assignable to a `Reader` variable unless it also happens to have the methods the interface requires -- and when it does, that is the structural rule doing the work, not the embedding. **Rust** likewise has no implementation inheritance. Traits give the polymorphism, and default method bodies in a trait give the reuse a base class would have provided; supertraits express requirement, not code sharing. When you genuinely want to reuse an existing type's data layout, you compose or wrap. **TypeScript** flips the other axis: subtyping is structural, so a class that never mentions an interface still satisfies it, and two structurally identical classes are mutually assignable. Declaring `private` or `#` fields brands a class so that structural twins stop being interchangeable -- an opt-in back into nominality precisely because pure structure is too permissive for domain types. ## Why the distinction earns its keep Once you can name the two relations, several everyday decisions stop being matters of taste. - **"Should this extend that?"** becomes two questions: do I want the code, and do I want callers to substitute? If the answers differ, the language tells you the tool. In C++ that is private inheritance or a member; in Go, embedding; in Java, a field plus delegation, because Java cannot express reuse-without-subtyping. - **Published API surface.** In a language that welds the relations, every base class you extend for convenience becomes part of your type's public contract, including its protected members and its future changes. - **Interface inheritance is the cheap half.** Implementing an interface commits you to a signature set and nothing else; there is no inherited state, no constructor ordering, and no surprise when the parent changes. ## The trap to avoid in an interview Do not answer "just use composition". That is a conclusion, and it is one this leaf's sibling material argues at length. The question here is about the *relations*: which one a language checks, which one it grants, and which mechanism supplies each. A candidate who says "C++ private inheritance gives me the members without the conversion, Go embedding gives me the methods without any conformance claim, and Java gives me no way to separate them" has demonstrated the concept. A candidate who says "prefer composition" has repeated a slogan. ## A note on what the compiler is actually checking When a compiler accepts a subtype claim, it checks *shape*: member names and signatures, plus variance rules where they apply. It does not check that the subtype behaves like the supertype -- that is the discipline named by the Liskov Substitution Principle, and no mainstream compiler verifies it. So even the relation the type system does track is tracked only structurally; the behavioural half of "is-a" remains a claim you make and must test. ## Interview shape Define both relations in one sentence each, give one language that separates them by keyword (C++), one that separates them by having no inheritance at all (Go or Rust), and note that welding them together is why "inherit to reuse" leaks a promise.
- Java has no private inheritance. If you want a base class's code but not its substitutability, what do you actually do?Hold the would-be base as a private field and delegate the methods you want to expose -- composition with forwarding. You lose the ability to override the base's virtual methods and to touch its protected members, which is exactly what C++ private inheritance preserves, so the Java version is slightly weaker but also more decoupled. Making the wrapper implement a narrow interface of its own restores polymorphism where callers genuinely need it.
- Go's embedding promotes methods onto the outer type. Doesn't that make the outer type substitutable for the embedded one?No. Promotion is a method-set operation, not a conversion: `Stack` gains `PushBack` but a `*Deque` parameter still rejects a `*Stack`. What promotion can do is make the outer type satisfy an *interface* that the embedded type satisfied, because the promoted methods are in its method set -- so substitutability appears only through the structural interface rule, and only for the interface, never for the concrete embedded type.
- TypeScript accepts a class as an implementation of an interface it never mentions. When is that a problem, and what is the standard fix?It is a problem when two types share a shape but not a meaning -- a `UserId` and an `OrderId` that are both `{ value: string }` become interchangeable, and a transposition bug type-checks. The standard fix is branding: give each type a `private` or `#` field, or a phantom literal property, so structural comparison fails and the type behaves nominally. That is opting back into declaration-based conformance for the cases where shape is not identity.
Subtyping is being accepted at a checkpoint because you carry the right credentials; subclassing is having been trained at the same academy. Some organisations issue credentials on graduation and the two look identical -- until someone trains there and is deliberately denied the badge.
saying these in an interview costs you the question
- Answering only "prefer composition over inheritance" -- a conclusion, not the distinction the question asks for.
- Claiming Go embedding makes the outer type usable wherever the embedded type is expected.
- Believing every language grants both relations with one keyword, so the distinction is only theoretical.
- Saying interface implementation is "inheritance without code" and stopping there, missing that structural languages need no declaration at all.