AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

10 Inheritance and polymorphism / Lesson 6 of 6

Composition versus inheritance — is-a, has-a and the shapes to avoid

14 min · +10 XP · 2 exercises · 6 questions

Read the text14 min
Exercises0/2
Quiz6 q

Quick answer

Should I use composition or inheritance in C++?

In C++, prefer composition — holding the other type as a member — and use public inheritance only when you need run-time polymorphism through a base interface. Public inheritance promises that the derived class is-a base and can substitute for it everywhere, invariants included, and couples it to the base's representation. Composition expresses has-a: you choose what to expose, can swap the part at run time and stay decoupled.

Inheritance is the most expensive relationship in C++: it couples the derived class to the base's representation, promises that a derived object can stand in for a base one everywhere, and cannot be undone without touching every user. Composition — a member object — makes none of those promises. This lesson gives the test for choosing between them (is-a versus has-a, and what "is-a" actually obliges you to), shows the classic square-and-rectangle failure, covers the two forms of inheritance that are not is-a — private and multiple — and states the rule the Core Guidelines and thirty years of practice agree on: prefer composition, and reach for public inheritance only to get run-time polymorphism through a base interface.

is-a versus has-a

class Engine {
public:
    void start() { running_ = true; }
    bool running() const { return running_; }
private:
    bool running_ = false;
};

class Car {                          // a Car HAS an Engine
public:
    void start() { engine_.start(); }
    bool running() const { return engine_.running(); }
private:
    Engine engine_;
};

class ElectricCar : public Car {};   // an ElectricCar IS a Car

A Car is not an Engine; handing a Car to a function that expects an Engine& would be nonsense. So the engine is a member, and Car delegates — start() forwards to engine_.start(). Composition lets Car decide what to expose: nothing of Engine's interface leaks unless Car chooses to forward it. Public inheritance exposes everything public in the base, and it makes a promise composition does not: an ElectricCar can be used wherever a Car& is accepted, and every function that works on a Car must keep working.

The quick test is to say the sentence aloud. "A car is an engine" fails; "a car has an engine" passes. When the sentence is ambiguous — "a stack is a vector"? — the harder test decides.

The substitution rule

The Liskov substitution principle is the harder test: a derived class must honour every promise the base makes, so that code written against the base keeps working when handed a derived object. Not "has the same functions" — behaves compatibly. The classic failure:

class Rectangle {
public:
    Rectangle(int w, int h) : w_(w), h_(h) {}
    virtual ~Rectangle() = default;
    virtual void setWidth(int w)  { w_ = w; }
    virtual void setHeight(int h) { h_ = h; }
    int area() const { return w_ * h_; }
protected:
    int w_, h_;
};

class Square : public Rectangle {                // a square IS a rectangle... mathematically
public:
    explicit Square(int side) : Rectangle(side, side) {}
    void setWidth(int w) override  { w_ = h_ = w; }     // keep it square
    void setHeight(int h) override { w_ = h_ = h; }
};

void stretch(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(2);
    std::cout << r.area() << '\n';        // a Rectangle prints 10
}

Square sq(3);
stretch(sq);                              // prints 4

stretch was written against Rectangle's promise: after setWidth(5) and setHeight(2) the area is 10. A Square cannot keep that promise — keeping its own invariant (sides equal) breaks the base's (width and height independent). The mathematics says a square is a rectangle; the mutable interface says it is not. Either drop the setters — an immutable Rectangle and an immutable Square substitute fine, because nothing can be broken — or do not derive. The general lesson: is-a is decided by the base's interface and invariants, not by the nouns.

Why composition wins by default

  • Coupling. A derived class depends on the base's protected members and on the behaviour of every virtual function it does not override. Changing the base can break the derived class without touching its source — the fragile base class problem. A member's interface is a smaller, published surface.
  • Choice of interface. Composition exposes what you forward; inheritance exposes the base's whole public interface, including functions that make no sense for the derived type.
  • Flexibility. A member can be swapped at run time — a std::unique_ptr<Logger> that points at a file logger or a console logger — while a base class is fixed at compile time.
  • Testing. A class whose collaborators are members can be given test doubles; one whose collaborator is its base cannot.

The Java standard library's Stack extends Vector is the cautionary tale: every Stack exposes insertElementAt, so nothing guarantees it is a stack. C++ chose composition for std::stack: it contains a std::deque and forwards push, pop and top. Do the same with std::vector — a class that needs a vector holds one; it does not derive from one, which would also inherit a non-virtual destructor (lesson 4).

Private inheritance

class Stack : private std::vector<int> is legal and means "implemented in terms of": the base's public members become private in Stack, no one outside can treat a Stack as a std::vector<int>, and the relationship is invisible to users. It is composition wearing inheritance's syntax.

class Stack : private std::vector<int> {
public:
    using std::vector<int>::empty;              // re-expose selected members
    using std::vector<int>::size;
    void push(int v) { push_back(v); }
    int pop() { int v = back(); pop_back(); return v; }
};

Prefer a member. Private inheritance earns its place in three narrow cases: the derived class must override a virtual function of the implementation class; it needs access to that class's protected members; or the base is empty and the empty base optimisation saves the byte a member would cost — a concern for library authors, not application code.

Multiple inheritance and the diamond

A class may have several bases. With interface classes (lesson 3) it is routine and safe: interfaces carry no state, so nothing can collide. With bases that carry data, a shape called the diamond appears:

struct Device  { std::string id; };
struct Printer : Device { void print() {} };
struct Scanner : Device { void scan() {} };
struct Copier  : Printer, Scanner {};

Copier c;
c.id = "x";           // error: request for member 'id' is ambiguous — two Device subobjects
c.Printer::id = "x";  // one of them

A Copier contains two Devices, one through each path, and c.id cannot choose. Virtual inheritance makes the paths share one:

struct Printer : virtual Device { void print() {} };
struct Scanner : virtual Device { void scan() {} };
struct Copier  : Printer, Scanner {};       // one Device; c.id is unambiguous

With virtual bases the most derived class constructs the shared base — Copier's initialiser list is where Device(…) is called, and the Device(…) in Printer's and Scanner's lists is ignored when they are parts of a Copier — and each object gains a pointer to locate the shared subobject. The standard library's own diamond, std::iostream deriving from std::istream and std::ostream, which both derive virtually from std::ios, is why the mechanism exists. It is rare in application code; when a diamond appears in a design, the usual answer is that one of the edges should have been composition.

Choosing

Use public inheritance whenUse composition when
Callers must handle objects through a base interface without knowing the concrete typeYou want to reuse an implementation
The derived type honours every promise of the base, invariants includedThe derived type would have to hide or weaken base functions
The set of types is open and the operations are fixed (lesson 5)The collaborator may change at run time or be a test double
The base is an interface: pure virtual functions and no dataThe relationship is "uses" or "has", not "is"

The Core Guidelines summarise it as C.129: when designing a class hierarchy, distinguish between implementation inheritance and interface inheritance — and use public inheritance for the interface kind only.

Pitfalls

MistakeWhat happens
Deriving to reuse code (class Stack : public std::vector<int>)The whole base interface leaks; no virtual destructor.
Square : Rectangle with settersSubstitution fails; functions written for the base compute wrong answers.
Two data-carrying bases sharing a grandparentTwo copies of the grandparent; ambiguous member access.
Protected data reached from many derived classesEvery change to the base ripples through all of them.
Multiple non-interface basesDiamonds, ambiguity and initialisation puzzles; keep to one base plus interfaces.

Key takeaways

  • Public inheritance says is-a and substitutable: the derived class must keep every promise of the base's interface, invariants included.
  • Composition (a member) says has-a: you choose what to expose, can swap the part, and stay decoupled from its representation.
  • Prefer composition; use public inheritance to get dynamic dispatch through an interface class.
  • Private inheritance is composition in disguise — "implemented in terms of" — and rarely better than a member.
  • Multiple inheritance is safe for interfaces; data-carrying diamonds need virtual bases and are usually a design mistake.

Common questions

What is the Liskov substitution principle?

The Liskov substitution principle says a derived class must honour every promise its base makes, so that code written against the base keeps working when handed a derived object. Having the same functions is not enough: it must behave compatibly, preserving the base's invariants.

Why is a square not a rectangle in object-oriented design?

A mutable Rectangle promises that setWidth and setHeight change the sides independently, so code that sets width 5 and height 2 expects area 10. A Square must keep its sides equal and breaks that promise, giving 4 instead. Drop the setters and make both immutable, or do not derive.

What is the diamond problem in C++?

When a class derives from two classes that share a base, it contains two copies of that base, and accessing their members is ambiguous. Virtual inheritance (: virtual Device) makes the paths share one subobject, constructed by the most derived class. std::iostream is the standard library's own diamond.

What is private inheritance used for in C++?

Private inheritance means "implemented in terms of": the base's members become private and outside code cannot treat the derived class as the base. A member usually does the same job more clearly; private inheritance earns its place only to override a virtual function of the implementation, reach its protected members, or benefit from the empty base optimisation.

Why should you not inherit from std::vector?

Public inheritance would expose every std::vector operation, including ones that break your class's rules, and std::vector has no virtual destructor, so deleting through a std::vector* is undefined behaviour. Hold a vector as a member and forward the operations you want, as std::stack does with its underlying container.

Mark as readSign in to keep your place.

Composition vs Inheritance in C++: Is-a, Has-a and LSP

Prefer composition in C++: public inheritance promises is-a and substitutability. The Liskov principle, the square-rectangle trap and the diamond.

  • Course: C++ study plan
  • Module: Inheritance and polymorphism
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

Should I use composition or inheritance in C++?

In C++, prefer composition — holding the other type as a member — and use public inheritance only when you need run-time polymorphism through a base interface. Public inheritance promises that the derived class is-a base and can substitute for it everywhere, invariants included, and couples it to the base's representation. Composition expresses has-a: you choose what to expose, can swap the part at run time and stay decoupled.

Lesson

Inheritance is the most expensive relationship in C++: it couples the derived class to the base's representation, promises that a derived object can stand in for a base one everywhere, and cannot be undone without touching every user. Composition — a member object — makes none of those promises. This lesson gives the test for choosing between them (is-a versus has-a, and what "is-a" actually obliges you to), shows the classic square-and-rectangle failure, covers the two forms of inheritance that are not is-a — private and multiple — and states the rule the Core Guidelines and thirty years of practice agree on: prefer composition, and reach for public inheritance only to get run-time polymorphism through a base interface.

is-a versus has-a

class Engine {
public:
    void start() { running_ = true; }
    bool running() const { return running_; }
private:
    bool running_ = false;
};

class Car {                          // a Car HAS an Engine
public:
    void start() { engine_.start(); }
    bool running() const { return engine_.running(); }
private:
    Engine engine_;
};

class ElectricCar : public Car {};   // an ElectricCar IS a Car

A Car is not an Engine; handing a Car to a function that expects an Engine& would be nonsense. So the engine is a member, and Car delegates — start() forwards to engine_.start(). Composition lets Car decide what to expose: nothing of Engine's interface leaks unless Car chooses to forward it. Public inheritance exposes everything public in the base, and it makes a promise composition does not: an ElectricCar can be used wherever a Car& is accepted, and every function that works on a Car must keep working.

The quick test is to say the sentence aloud. "A car is an engine" fails; "a car has an engine" passes. When the sentence is ambiguous — "a stack is a vector"? — the harder test decides.

The substitution rule

The Liskov substitution principle is the harder test: a derived class must honour every promise the base makes, so that code written against the base keeps working when handed a derived object. Not "has the same functions" — behaves compatibly. The classic failure:

class Rectangle {
public:
    Rectangle(int w, int h) : w_(w), h_(h) {}
    virtual ~Rectangle() = default;
    virtual void setWidth(int w)  { w_ = w; }
    virtual void setHeight(int h) { h_ = h; }
    int area() const { return w_ * h_; }
protected:
    int w_, h_;
};

class Square : public Rectangle {                // a square IS a rectangle... mathematically
public:
    explicit Square(int side) : Rectangle(side, side) {}
    void setWidth(int w) override  { w_ = h_ = w; }     // keep it square
    void setHeight(int h) override { w_ = h_ = h; }
};

void stretch(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(2);
    std::cout << r.area() << '\n';        // a Rectangle prints 10
}

Square sq(3);
stretch(sq);                              // prints 4

stretch was written against Rectangle's promise: after setWidth(5) and setHeight(2) the area is 10. A Square cannot keep that promise — keeping its own invariant (sides equal) breaks the base's (width and height independent). The mathematics says a square is a rectangle; the mutable interface says it is not. Either drop the setters — an immutable Rectangle and an immutable Square substitute fine, because nothing can be broken — or do not derive. The general lesson: is-a is decided by the base's interface and invariants, not by the nouns.

Why composition wins by default

  • Coupling. A derived class depends on the base's protected members and on the behaviour of every virtual function it does not override. Changing the base can break the derived class without touching its source — the fragile base class problem. A member's interface is a smaller, published surface.
  • Choice of interface. Composition exposes what you forward; inheritance exposes the base's whole public interface, including functions that make no sense for the derived type.
  • Flexibility. A member can be swapped at run time — a std::unique_ptr<Logger> that points at a file logger or a console logger — while a base class is fixed at compile time.
  • Testing. A class whose collaborators are members can be given test doubles; one whose collaborator is its base cannot.

The Java standard library's Stack extends Vector is the cautionary tale: every Stack exposes insertElementAt, so nothing guarantees it is a stack. C++ chose composition for std::stack: it contains a std::deque and forwards push, pop and top. Do the same with std::vector — a class that needs a vector holds one; it does not derive from one, which would also inherit a non-virtual destructor (lesson 4).

Private inheritance

class Stack : private std::vector<int> is legal and means "implemented in terms of": the base's public members become private in Stack, no one outside can treat a Stack as a std::vector<int>, and the relationship is invisible to users. It is composition wearing inheritance's syntax.

class Stack : private std::vector<int> {
public:
    using std::vector<int>::empty;              // re-expose selected members
    using std::vector<int>::size;
    void push(int v) { push_back(v); }
    int pop() { int v = back(); pop_back(); return v; }
};

Prefer a member. Private inheritance earns its place in three narrow cases: the derived class must override a virtual function of the implementation class; it needs access to that class's protected members; or the base is empty and the empty base optimisation saves the byte a member would cost — a concern for library authors, not application code.

Multiple inheritance and the diamond

A class may have several bases. With interface classes (lesson 3) it is routine and safe: interfaces carry no state, so nothing can collide. With bases that carry data, a shape called the diamond appears:

struct Device  { std::string id; };
struct Printer : Device { void print() {} };
struct Scanner : Device { void scan() {} };
struct Copier  : Printer, Scanner {};

Copier c;
c.id = "x";           // error: request for member 'id' is ambiguous — two Device subobjects
c.Printer::id = "x";  // one of them

A Copier contains two Devices, one through each path, and c.id cannot choose. Virtual inheritance makes the paths share one:

struct Printer : virtual Device { void print() {} };
struct Scanner : virtual Device { void scan() {} };
struct Copier  : Printer, Scanner {};       // one Device; c.id is unambiguous

With virtual bases the most derived class constructs the shared base — Copier's initialiser list is where Device(…) is called, and the Device(…) in Printer's and Scanner's lists is ignored when they are parts of a Copier — and each object gains a pointer to locate the shared subobject. The standard library's own diamond, std::iostream deriving from std::istream and std::ostream, which both derive virtually from std::ios, is why the mechanism exists. It is rare in application code; when a diamond appears in a design, the usual answer is that one of the edges should have been composition.

Choosing

Use public inheritance whenUse composition when
Callers must handle objects through a base interface without knowing the concrete typeYou want to reuse an implementation
The derived type honours every promise of the base, invariants includedThe derived type would have to hide or weaken base functions
The set of types is open and the operations are fixed (lesson 5)The collaborator may change at run time or be a test double
The base is an interface: pure virtual functions and no dataThe relationship is "uses" or "has", not "is"

The Core Guidelines summarise it as C.129: when designing a class hierarchy, distinguish between implementation inheritance and interface inheritance — and use public inheritance for the interface kind only.

Pitfalls

MistakeWhat happens
Deriving to reuse code (class Stack : public std::vector<int>)The whole base interface leaks; no virtual destructor.
Square : Rectangle with settersSubstitution fails; functions written for the base compute wrong answers.
Two data-carrying bases sharing a grandparentTwo copies of the grandparent; ambiguous member access.
Protected data reached from many derived classesEvery change to the base ripples through all of them.
Multiple non-interface basesDiamonds, ambiguity and initialisation puzzles; keep to one base plus interfaces.

Key takeaways

  • Public inheritance says is-a and substitutable: the derived class must keep every promise of the base's interface, invariants included.
  • Composition (a member) says has-a: you choose what to expose, can swap the part, and stay decoupled from its representation.
  • Prefer composition; use public inheritance to get dynamic dispatch through an interface class.
  • Private inheritance is composition in disguise — "implemented in terms of" — and rarely better than a member.
  • Multiple inheritance is safe for interfaces; data-carrying diamonds need virtual bases and are usually a design mistake.

Common questions

What is the Liskov substitution principle?

The Liskov substitution principle says a derived class must honour every promise its base makes, so that code written against the base keeps working when handed a derived object. Having the same functions is not enough: it must behave compatibly, preserving the base's invariants.

Why is a square not a rectangle in object-oriented design?

A mutable Rectangle promises that setWidth and setHeight change the sides independently, so code that sets width 5 and height 2 expects area 10. A Square must keep its sides equal and breaks that promise, giving 4 instead. Drop the setters and make both immutable, or do not derive.

What is the diamond problem in C++?

When a class derives from two classes that share a base, it contains two copies of that base, and accessing their members is ambiguous. Virtual inheritance (: virtual Device) makes the paths share one subobject, constructed by the most derived class. std::iostream is the standard library's own diamond.

What is private inheritance used for in C++?

Private inheritance means "implemented in terms of": the base's members become private and outside code cannot treat the derived class as the base. A member usually does the same job more clearly; private inheritance earns its place only to override a virtual function of the implementation, reach its protected members, or benefit from the empty base optimisation.

Why should you not inherit from std::vector?

Public inheritance would expose every std::vector operation, including ones that break your class's rules, and std::vector has no virtual destructor, so deleting through a std::vector* is undefined behaviour. Hold a vector as a member and forward the operations you want, as std::stack does with its underlying container.

Exercises

A service that owns its logger

Write Logger with void info(const std::string&), void error(const std::string&), a const std::vector<std::string>& lines() const accessor and int infoCount() const / int errorCount() const. Each call appends [<n>] INFO <message> or [<n>] ERROR <message>, where n is the line's 1-based number.

Write OrderService that has a Logger member (not a base class) and a std::map<std::string, int> of open orders, exposing the logger through const Logger& logger() const:

  • order <item> <qty> adds qty to the item and logs order <item> x<qty> at INFO.
  • cancel <item> removes the item and logs cancel <item> at INFO, or logs cancel <item>: not found at ERROR.
  • ship logs ship <orders> orders, <units> units at INFO (the number of open items and the sum of their quantities) and clears them.

Input: commands until the input ends. Output: every log line, then info=<n> error=<n>.

order pen 3
order ink 1
cancel cup
ship
cancel pen

prints

[1] INFO order pen x3
[2] INFO order ink x1
[3] ERROR cancel cup: not found
[4] INFO ship 2 orders, 4 units
[5] ERROR cancel pen: not found
info=3 error=2

Undo history from two vectors

Write History, a class composed of two std::vector<std::string> members — done_ and undone_ — and nothing else. It is not a vector and must not derive from one; it exposes exactly:

  • void apply(const std::string& action) — pushes onto done_ and clears undone_ (a new action discards the redo trail).
  • bool canUndo() const / std::string undo() — moves the most recent action from done_ to undone_ and returns it.
  • bool canRedo() const / std::string redo() — the reverse.
  • std::string show() const — the done actions oldest first, separated by single spaces, or (empty).

Input: commands until the input ends: do <word>, undo, redo, show. Output: do prints nothing; undo prints undo <word> or nothing to undo; redo prints redo <word> or nothing to redo; show prints the list. At the end print undoable=<n> redoable=<n>.

do type
do bold
undo
show
redo
do italic
redo
show

prints

undo bold
type
redo bold
nothing to redo
type bold italic
undoable=3 redoable=0

In this module: Inheritance and polymorphism

← RTTI and the vtable — how dispatch works and when to ask an object its type · Checkpoint — Inheritance and polymorphism →