Bank 6 — Polymorphism and interfaces¶
Twelve exercises on run-time dispatch through a base type, and on interfaces as promises the compiler enforces. Modules 7 and 8. Levels L1–L6.
Solutions are in Solutions and answers; nudges are in Hints. Attempt first.
Exercises¶
E59 — Why a pointer? L1 concept¶
Task. In one sentence each: (a) what virtual changes about a member function call;
(b) why a call through a reference or pointer dispatches to the derived version while a
call on a value usually does not; (c) why an abstract class can be a function
parameter's type but cannot be a variable's type.
E60 — Two calls, two answers L2 predict¶
#include <iostream>
class Base {
public:
void greet() const { std::cout << "base\n"; }
virtual void hello() const { std::cout << "base\n"; }
};
class Derived : public Base {
public:
void greet() const { std::cout << "derived\n"; }
void hello() const override { std::cout << "derived\n"; }
};
int main() {
Derived d;
const Base& ref = d;
ref.greet();
ref.hello();
d.greet();
}
Task. Write the three output lines. Then state, in one sentence, the rule that makes the first and second lines differ.
E61 — Make it dispatch L2 complete¶
#include <iostream>
class Shape {
public:
// TODO (1): make area dispatch to the derived type, and make Shape abstract
// TODO (2): give Shape a destructor suitable for deletion through a Shape*
double area() const { return 0.0; }
};
class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
// TODO (3): implement area for Square, marking it so the compiler checks it
double area() const { return side_ * side_; }
private:
double side_;
};
int main() {
Square s{3.0};
const Shape& ref = s;
std::cout << ref.area() << '\n';
}
Task. Make the three changes so the program prints 9 and Shape becomes abstract.
Name the keyword in (3) and what it protects against.
E62 — All the shapes L2 trace scenario¶
A drawing tool stores several shapes through base pointers.
#include <iostream>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Rect : public Shape {
public:
Rect(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
private:
double w_, h_;
};
class Square : public Shape {
public:
explicit Square(double s) : s_(s) {}
double area() const override { return s_ * s_; }
private:
double s_;
};
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Rect{2.0, 5.0});
shapes.push_back(new Square{3.0});
double total = 0.0;
for (const Shape* s : shapes) total += s->area();
std::cout << total << '\n';
for (Shape* s : shapes) delete s;
}
Task. Give the printed total. Then explain which area() is called for each element
and why the loop does not need to know the concrete types.
E63 — Add a kind, change nothing L3 modify scenario¶
The drawing tool from E62 gains a Triangle (base times height over two). The rule is
that adding a shape must not require editing any existing class.
Task. Write the Triangle class, then answer: what did you have to change in the
loop, the Shape class, or the existing shapes, to add it? If your answer is "nothing",
say precisely which part of the design made that possible.
E64 — The zoo with no voices L3 debug scenario¶
An animal shelter stores animals in a std::vector and prints each one's sound.
#include <iostream>
#include <string>
#include <vector>
class Animal {
public:
virtual std::string speak() const { return "..."; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
std::string speak() const override { return "woof"; }
};
int main() {
std::vector<Animal> zoo;
zoo.push_back(Dog{});
std::cout << zoo[0].speak() << '\n';
}
Task. It prints ..., not woof. Name the phenomenon, explain why storing by value
destroys the derived part, and fix the container so the sound is woof. State what the
container now stores, and one cost of that change.
E65 — The destructor that is not called L4 predict¶
#include <iostream>
class Base {
public:
~Base() { std::cout << "~Base "; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "~Derived "; }
};
int main() {
Base* p = new Derived;
delete p;
}
Task. State which destructor(s) run in practice, then state precisely why this program has undefined behaviour (not merely "it leaks"). Fix it with one keyword, and say what the output becomes.
E66 — Do not make everything virtual L4 analyze¶
A new team member, having just learned about virtual, makes every member function
virtual, including private helpers and small accessors.
Task. Give three concrete reasons this is a poor default, at least one of which is about correctness rather than speed. Then state the rule you would use to decide whether a function should be virtual.
E67 — Cannot instantiate an abstract type L3 debug¶
#include <iostream>
class Report {
public:
virtual std::string title() const = 0;
virtual void render() const { std::cout << title() << '\n'; }
};
class Summary : public Report {
public:
std::string title() const override { return "Summary"; }
// render() inherited
};
int main() {
Report r; // line A
Summary s;
s.render();
}
Task. Explain the error on line A in one sentence: which function is unimplemented,
and why does that make the whole type un-instantiable even though render() has a body?
Then fix the program the smallest way, and confirm s.render() prints Summary.
E68 — Dispatch in a constructor L4 predict¶
#include <iostream>
class Base {
public:
Base() { describe(); } // a virtual call in a constructor
virtual void describe() const { std::cout << "base\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void describe() const override { std::cout << "derived\n"; }
};
int main() {
Derived d;
}
Task. Write the output, then explain why a Derived object printed the base
message. What does this tell you about calling a virtual function (or a pure virtual) from
a constructor, and what is the general rule for constructors and destructors?
E69 — Design a player interface L5 design poly scenario¶
A kiosk plays media from several sources — a local file, a stream, a playlist. The
kiosk only ever holds a MediaSource* and calls the same four operations: open,
play, pause, stop. Each source behaves differently internally.
Task. Design the interface and its contract. Give:
- the interface's name and its four operations as pure virtual functions (signatures
only), each with a one-sentence contract (preconditions, postconditions, and what
playdoes when the source is not open); - one rule about error handling that every implementation must respect;
- a destructor decision and why;
- a second interface for sources that can also seek, and why it must be separate rather than added to the first.
How you will be judged. On the contracts: an interface is a promise, and yours must state what the caller may rely on.
E70 — Kill the type switch L6 refactor scenario¶
A notification system uses an integer tag and a switch:
enum class Kind { Email, Sms, Push };
void send(Kind kind, const std::string& to, const std::string& body) {
switch (kind) {
case Kind::Email: /* ... */ break;
case Kind::Sms: /* ... */ break;
case Kind::Push: /* ... */ break;
}
}
Every new channel forces an edit here, and the same switch is repeated in three other
functions (format, cost, retry).
Task. Write a refactoring plan that replaces the tags with polymorphism. It must:
- name the interface and one class per channel;
- list the four functions that currently switch, and where their logic goes instead;
- say what happens to the
enum class Kindand to any code that persists a tag; - name the cost of the change (what becomes harder than it was);
- say how you would confirm no channel changed behaviour.
Success. The four switches collapse into one interface, and you have been honest about the trade-off.
Previous topic: Inheritance · Next topic: Composition and association · Hints: hints.md · Solutions: solutions.md