Inheritance & Is-A Relationships
Learn public inheritance, constructor chaining, the slicing problem, multiple inheritance, and the diamond problem — and when inheritance is appropriate.
Inheritance Models Is-A
Public inheritance models an "is-a" relationship: a Dog is an Animal, a SavingsAccount is a BankAccount. This is governed by the Liskov Substitution Principle (LSP): anywhere a base class is expected, a derived class must work correctly without surprises.
Inheritance is one of the most powerful — and most misused — features of OOP. Not every relationship is "is-a." A Stack is not a Vector even though it uses one internally. Misusing inheritance where composition is appropriate leads to fragile, hard-to-maintain code.
Basic Inheritance & Constructor Chaining
A derived class inherits all members of the base class. The base class constructor runs first, then the derived class constructor. Destruction happens in reverse order.
#include <iostream>
#include <string>
class Animal {
std::string name_;
int age_;
public:
Animal(const std::string& name, int age)
: name_{name}, age_{age}
{
std::cout << "Animal constructed: " << name_ << '\n';
}
virtual ~Animal() {
std::cout << "Animal destroyed: " << name_ << '\n';
}
const std::string& name() const { return name_; }
int age() const { return age_; }
virtual void speak() const {
std::cout << name_ << " makes a sound\n";
}
};
class Dog : public Animal { // public inheritance = is-a
std::string breed_;
public:
Dog(const std::string& name, int age, const std::string& breed)
: Animal{name, age} // base class constructor MUST be called first
, breed_{breed}
{
std::cout << "Dog constructed: " << name() << '\n';
}
~Dog() override {
std::cout << "Dog destroyed: " << name() << '\n';
}
void speak() const override {
std::cout << name() << " barks! (" << breed_ << ")\n";
}
};
int main() {
Dog rex{"Rex", 5, "German Shepherd"};
rex.speak(); // Rex barks! (German Shepherd)
// Destruction order: Dog destroyed, then Animal destroyed
return 0;
}The Slicing Problem
Object slicing occurs when a derived object is assigned or passed by value to a base class variable. The derived part is "sliced off" — only the base portion remains. This is a subtle and common bug.
#include <iostream>
#include <string>
#include <vector>
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual std::string type() const { return "Shape"; }
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_{r} {}
std::string type() const override { return "Circle"; }
};
// BUG: passing by value slices the object!
void print_type_bad(Shape s) {
std::cout << s.type() << '\n'; // always prints "Shape"!
}
// CORRECT: pass by reference to preserve polymorphism
void print_type_good(const Shape& s) {
std::cout << s.type() << '\n'; // prints actual type
}
int main() {
Circle c{5.0};
print_type_bad(c); // "Shape" — SLICED!
print_type_good(c); // "Circle" — correct
// Slicing also happens with containers of values:
// std::vector<Shape> shapes; // BAD: slicing on insert
// Use pointers instead:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(3.0));
std::cout << shapes[0]->type() << '\n'; // "Circle"
return 0;
}Multiple Inheritance & the Diamond Problem
C++ allows a class to inherit from multiple bases. The diamond problem occurs when two bases share a common ancestor, causing the derived class to contain two copies of the ancestor's data. Virtual inheritance solves this.
#include <iostream>
// Diamond problem:
// Device
// / \
// Printer Scanner
// \ /
// AllInOne
class Device {
protected:
int id_;
public:
Device(int id) : id_{id} {
std::cout << "Device(" << id_ << ")\n";
}
virtual ~Device() = default;
};
// Virtual inheritance: only ONE copy of Device in AllInOne
class Printer : virtual public Device {
public:
Printer(int id) : Device{id} {
std::cout << "Printer\n";
}
void print_doc() { std::cout << "Printing on device " << id_ << '\n'; }
};
class Scanner : virtual public Device {
public:
Scanner(int id) : Device{id} {
std::cout << "Scanner\n";
}
void scan_doc() { std::cout << "Scanning on device " << id_ << '\n'; }
};
class AllInOne : public Printer, public Scanner {
public:
// With virtual inheritance, the most-derived class
// must initialize the virtual base directly
AllInOne(int id) : Device{id}, Printer{id}, Scanner{id} {
std::cout << "AllInOne\n";
}
};
int main() {
AllInOne aio{42};
aio.print_doc(); // Printing on device 42
aio.scan_doc(); // Scanning on device 42
// Only ONE Device sub-object exists — no ambiguity
return 0;
}Covariant Return Types & Protected Access
Covariant return types let a derived class override a virtual function and return a more specific (derived) pointer or reference type. For example, if Base::clone() returns Base*, Derived::clone() can return Derived*.
The protected access specifier sits between public and private: protected members are accessible to the class itself and its derived classes, but not to outside code. Use protected for members that derived classes legitimately need — but be cautious, as it creates a coupling between base and derived.
A common mistake is using inheritance solely to reuse code. If a Stack inherits from std::vector, users can call push_back, insert, and other vector operations that violate the stack abstraction. Inheritance should model a true is-a relationship satisfying the Liskov Substitution Principle. For code reuse, prefer composition (a Stack that *contains* a vector).
Always use public inheritance — private/protected inheritance is rarely needed and confusing. Always declare base class destructors as virtual. Pass polymorphic objects by reference or pointer, never by value (to avoid slicing). Prefer composition over inheritance unless the relationship is truly "is-a." Avoid deep inheritance hierarchies (2-3 levels is usually the practical limit).
- Public inheritance models is-a — the derived class must satisfy the Liskov Substitution Principle
- Base constructors run first, destructors run last (reverse order of construction)
- Object slicing occurs when a derived object is passed by value to a base — always use references or pointers
- The diamond problem is solved with
virtualinheritance, but prefer composition instead - Inheritance is for substitutability, not code reuse — use composition for the latter
Quiz — Test Your Knowledge
(15 XP)1. What is object slicing?
2. How does virtual inheritance solve the diamond problem?
3. Why should you NOT inherit from `std::vector` to create a `Stack` class?