C++ / INHERITANCE AND POLYMORPHISM
Inheritance and modelling is-a relationships
Model is-a relationships with public inheritance in C++: build a derived class, initialize its base subobject, and know when composition fits better.
What you will learn
- Declare a derived class and initialize its base subobject from the member init list.
- Trace construction order: base first, derived members, then the derived body.
- Call inherited public members on a derived object without redeclaring them.
- Apply the substitution test to choose between public inheritance and a member.
Understanding Inheritance and modelling is-a relationships
A derived class does not copy the base class, it embeds it. When you write class Manager : public Employee, every Manager object physically contains a complete Employee subobject, and that subobject must be fully built before any Manager-specific member exists. That is why base initialization belongs in the member initializer list and runs before the derived constructor body, and why destruction runs the other way: the derived part is torn down first, while the base it depends on is still valid.
Public inheritance also declares a conversion. The compiler binds a Manager reference to a const Employee& parameter, and a Manager* to an Employee*, with no cast, because the Employee it needs is sitting inside the object. The consequence is that every function ever written against Employee now silently accepts Managers, so the question to ask before writing : public Employee is not whether the two classes share fields, but whether every statement a caller may legally write against an Employee stays correct when the object is really a Manager.
Reuse alone is not a reason to inherit. If a class merely needs another class's services, a GameLoop that times frames or a Stack that stores its elements in a vector, make it a member and expose only the operations you are willing to support, because public inheritance permanently publishes the base's entire public interface as part of yours. Say the sentence out loud as a test: "a Manager is an Employee" is a claim about how the object may be used, while "a Stack is a vector" is not, and the shortcut ends with you defending invariants against inherited functions like clear() and insert().
<iostream>
<string>
<utility>
class Employee {
public:
Employee(std::string name, int id)
: name_(std::move(name)), id_(id) {
std::cout << "Employee(" << name_ << ") built\n";
}
~Employee() { std::cout << "Employee(" << name_ << ") gone\n"; }
const std::string& name() const { return name_; }
void printBadge() const {
std::cout << "badge #" << id_ << " " << name_ << "\n";
}
private:
std::string name_;
int id_;
};
class Manager : public Employee {
public:
Manager(std::string name, int id, int reports)
: Employee(std::move(name), id), reports_(reports) {
std::cout << "Manager(" << name() << ") built\n";
}
~Manager() { std::cout << "Manager(" << name() << ") gone\n"; }
int reports() const { return reports_; }
private:
int reports_;
};
void checkIn(const Employee& e) { // accepts any Employee, Manager included
e.printBadge();
}
int main() {
Manager m("Dana", 4102, 3);
checkIn(m);
std::cout << m.name() << " has " << m.reports() << " reports\n";
}
Public inheritance embeds a base subobject in every derived object and promises the derived object can be used wherever the base is expected, which makes it a claim about behaviour rather than a code-reuse tool.
Worked examples
When is-a is a lie
Square derived from Rectangle compiles and breaks, because Rectangle promises callers may change width alone.
<iostream>
class Rectangle {
public:
Rectangle(int w, int h) : w_(w), h_(h) {}
void setWidth(int w) { w_ = w; }
void setHeight(int h) { h_ = h; }
int area() const { return w_ * h_; }
private:
int w_;
int h_;
};
class Square : public Rectangle { // claims a Square is-a Rectangle
public:
explicit Square(int side) : Rectangle(side, side), side_(side) {}
int side() const { return side_; }
private:
int side_;
};
void stretch(Rectangle& r) { // legal for any Rectangle
r.setWidth(10);
}
int main() {
Square s(4);
std::cout << "before: side=" << s.side() << " area=" << s.area() << "\n";
stretch(s);
std::cout << "after: side=" << s.side() << " area=" << s.area() << "\n";
}
Example explained
Line 1Rectangle(side, side) in the init list is the only way to build the base subobject; a Square cannot skip it.
Line 2stretch takes Rectangle&, and public inheritance converts Square& to it without a cast, so the call compiles.
Line 3setWidth(10) edits the base subobject only, leaving side_ at 4, so the object now reports side 4 and area 40.
Line 4Nothing is wrong with the code; the is-a claim was wrong, because Square cannot honour Rectangle's promise of independent sides.
Extending without retyping the base
A derived class inherits the base constructor with a using declaration and builds new behaviour out of inherited operations.
<iostream>
class Account {
public:
explicit Account(long cents) : cents_(cents) {}
void deposit(long amount) { cents_ += amount; }
long balance() const { return cents_; }
private:
long cents_;
};
class SavingsAccount : public Account {
public:
using Account::Account; // reuse the base constructor
void addInterest(int basisPoints) {
deposit(balance() * basisPoints / 10000);
}
};
int main() {
SavingsAccount s(200000); // 2000.00 in cents
s.addInterest(250); // 2.50%
std::cout << "balance = " << s.balance() << "\n";
}
Example explained
Line 1using Account::Account; imports the base constructor, since constructors are not inherited by default.
Line 2addInterest calls deposit and balance() unqualified because inherited public members are part of SavingsAccount's own interface.
Line 3200000 * 250 / 10000 is 5000, and it is added through the base's deposit, so Account keeps sole control of cents_.
Line 4This is the case where inheritance earns its keep: the derived class only adds behaviour and contradicts nothing the base promised.
Important notes
Public inheritance is only one form; class D : private B and class D : protected B model "implemented in terms of" and give outside code no Derived-to-Base conversion. Note that private is the default for class and public for struct.
The base here has non-virtual functions and is used through references to concrete objects; the moment you delete derived objects through a base pointer, the base needs a virtual destructor, which is covered separately.
Common mistakes
Leaving the base out of the initializer list and assigning base fields in the derived constructor body: the base subobject is already constructed at that point, so you initialize twice, and if the base has no default constructor the code does not compile at all (no matching function for call to Base::Base()).
Inheriting for code reuse, as in class Stack : public std::vector<int>: every base operation becomes part of Stack's public interface, so a caller can insert() in the middle or clear() the container and destroy the invariant Stack existed to protect.
Expecting Derived d(args) to work because Base has such a constructor: constructors are not inherited unless you write using Base::Base; or a derived constructor that forwards to the base.
Try it yourself
Change, predict, then run
Add a Contractor class deriving from the Employee in the main example, giving it an hourly rate and passing name and id to Employee in the initializer list, then call checkIn(contractor) unchanged. Delete the Employee(...) entry from the initializer list and read the compiler error it produces.
Open the C++ workspaceCheck your understanding
A Timer class exposes start(), stop(), and elapsedMs(). You need a GameLoop that times frames internally but must never let callers start or stop that clock. Which design is right?
- Give GameLoop a Timer member and expose only the frame operations, because the relationship is has-a.
- Derive GameLoop publicly from Timer, since that reuses Timer's code without writing any forwarding functions.
- Derive GameLoop publicly from Timer and document that start() and stop() must not be called on a GameLoop.
- Derive GameLoop publicly from Timer, because a GameLoop clearly contains a Timer's data.
Show answer
Public inheritance adds start() and stop() to GameLoop's public interface for good and makes GameLoop convertible to Timer&, so any code holding a Timer& may stop the loop's clock; a member gives the same reuse while you choose what to expose. Options 2 and 4 mistake sharing data or saving typing for substitutability, and option 3 is worse: a comment removes neither the callable functions nor the implicit conversion.