Module 8 — Self-Assessment¶
Ten questions. Parts A and B are closed-book; Part C wants a compiler. Answers state what a full-credit answer contains, not merely the right word.
Part A — Recall and comprehension¶
1. What does = 0 mean on a member function, and what does it not mean?
(Answer: it makes the function pure virtual and the class abstract, so the class cannot be
instantiated and every concrete derived class must override it. It does not mean the
function has no implementation — a definition is allowed, and a subclass may call it by
qualification. It also does not mean the class cannot have data or non-virtual functions;
most abstract base classes have both. Full credit: the two statements = 0 makes, and the
observation that a destructor declared pure must still be defined, because it always
runs.)
2. Name the two shapes of abstraction in this module, and give the decision rule between them.
(Answer: an abstract base class — state plus partial implementation, with protected pure virtual extension points — and an interface — no data, all pure virtual functions plus a virtual destructor. Rule: use an abstract base when several types share data or a fixed algorithm with variable parts; use an interface when a caller needs a capability and nothing else. Full credit: the consequence — you may derive from several interfaces, whereas deriving from several stateful bases duplicates the state (Module 6's diamond).)
3. Write the four parts of a contract and give one example of each, from this module.
(Answer: precondition — what must hold before the call (amount > 0, the message is not
empty); postcondition — what is guaranteed on return (a reference that can be quoted, or a
refusal with a reason); invariant — what stays true for the object's life (a wallet's balance
is never negative). The fourth line an answer should have is the failure postcondition —
what happens when the precondition does not hold. Full credit: naming that the empty-message
rule lives in the service and the address rule in the channel, which is a contract split
along "who knows this".)
4. Why can a class with a pure virtual function not be instantiated, and what does the compiler tell you?
(Answer: the contract is incomplete — no implementation exists for at least one operation,
so there is no such thing as an object of that type. The compiler's error names the variable
and adds note: lines listing every pure virtual function still unimplemented, naming the
base's declaration, which is the fastest way to find a missed or mismatched override.)
5. Why is the interface-segregation test "can every implementation honour every function, without lying?" rather than "does every implementation compile?"
(Answer: a receipt printer that implements scan() can compile perfectly by returning
false or throwing, and both are lies the type system permits. Compilation tests syntax, not
promises. The consequence is that a wide interface relocates change rather than removing
it: callers stop changing, and implementers start lying. Full credit: naming that the fix is
to move the capability out (a second interface, or a capability query), and that the cost
moves to callers that must now ask.)
Part B — Prediction and explanation¶
6. What does this print, and what does the second line demonstrate?
class Shape {
public:
virtual ~Shape() = default;
virtual std::string name() const = 0;
virtual double area() const = 0;
std::string describe() const {
return name() + " area " + std::to_string(static_cast<int>(area()));
}
};
class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
std::string name() const override { return "square"; }
double area() const override { return side_ * side_; }
private:
double side_;
};
int main() {
Square s(3.0);
std::cout << s.describe() << '\n';
Shape* p = &s;
std::cout << p->describe() << '\n';
}
Answer.
Both lines are the same because describe() is not virtual and does not need to be: it
is one function, written once in the base, and it is correct for every subclass because the
functions it calls are virtual. The second line demonstrates the non-virtual interface
technique — the base owns the algorithm (describe's shape), the subclass supplies the
varying parts (name, area). Note that making describe() virtual would be a mistake
here: it would let a subclass change the format of every description, which is a guarantee
the base is entitled to keep. Full credit: both lines, the word "not virtual", and the
sentence about who owns the algorithm.
7. This program does not compile. There are two errors in it. Name both, say which line
each is reported on, and say which one override would have moved.
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual std::string label() const { return "shape"; }
};
class Circle : public Shape {
public:
explicit Circle(double r) : r_(r) {}
double area() { return 3.14159 * r_ * r_; } // (1)
std::string Label() const override { return "circle"; } // (2)
private:
double r_;
};
int main() {
Circle c(2.0);
std::cout << c.area() << ' ' << c.label();
}
Answer. (1) double area() is missing const, so it does not override the pure
double area() const — Circle stays abstract, and the error appears at main's use of
Circle (cannot declare variable 'c' to be of abstract type 'Circle', with a note:
naming Shape::area). A hiding warning is reported at line (1). (2) Label differs in name
from label, so override fails immediately: marked 'override', but does not override —
and because it is reported at the declaration, that is the error override moved. Full
credit: naming that (1)'s real error is at the use site, that (2) is caught at its own
line, and that the missing override on (1) is why (1)'s diagnosis is harder. Note also that
c.label() in main still compiles, because Shape::label() is non-pure and inherited.
8. What happens when this program is built, and why is the failure different in kind from every other mistake in this module?
class Sink {
public:
virtual ~Sink() = 0;
virtual void write(const std::string& message) = 0;
};
class EventSink : public Sink {
public:
~EventSink() override = default;
void write(const std::string& message) override { (void)message; }
};
int main() { EventSink sink; sink.write("x"); }
Answer. It compiles with no diagnostics and fails to link: EventSink's destructor
must call ~Sink(), and ~Sink() has no body. Every other mistake in this module is caught
by the compiler; this one is caught by the linker, because the declaration is complete and
legal and only the definition is missing. On the toolchain used by this course the message
names no symbol at all (collect2.exe: error: ld returned 116 exit status); elsewhere it
reads undefined reference to 'Sink::~Sink()'. The fix is Sink::~Sink() = default; — or, if
the intent is "never delete through a Sink*", replace the pure virtual destructor with a
protected non-virtual one, which fails at compile time with a message that names the
line. Full credit: compile-time success plus link failure plus the reasoning about why a
destructor's definition is not optional.
9. A colleague writes: "An interface is better if it has everything a caller might need, because then callers never have to change." Give the counter-argument, with the cost named from the implementer's side.
Answer. A wide interface does not remove change; it relocates it from callers — few and
known — to implementers — many, and including ones that do not exist yet. Every function in
an interface is a promise every concrete class must keep, so a Printer with scan() forces
a receipt printer to implement a scanner it does not have, and the permitted implementations
are all lies: throw, return false, or do nothing. The counter-example from this module is
Notifier: one function, three implementations including a recorder, and a fourth channel
that "changes no existing file". Full credit: naming the relocation of cost, giving a
concrete lying implementation, and stating the test — can every implementation honour every
function without lying? — plus acknowledging the real cost of splitting: callers that need
several capabilities must now name several types, or a combined interface must exist.
10. Here is an abstraction with four defects. Find them all.
class Logger {
public:
~Logger() = default;
virtual void write(const std::string& line) = 0;
virtual std::string name() const = 0;
std::string level_ = "info";
};
class FileLogger : public Logger {
public:
void write(const std::string& line) override { … }
std::string name() const override { return "file"; }
void write(const char* line) { write(std::string(line)); }
bool rotate(); // not virtual, not in the base
};
class NullLogger : public Logger {
public:
void write(const std::string&) override { /* discard */ }
std::string name() const override { return "null"; }
void record(const std::string& audit); // the null logger's own extra operation
};
void run(const std::vector<Logger>& loggers) {
for (const Logger& logger : loggers) logger.write("start");
}
Answer. (1) ~Logger() is not virtual, while Logger has pure virtual functions —
delete through a Logger* will not destroy the derived part, and the standard makes it
undefined behaviour. (2) The base has public data (level_ = "info"), so every subclass
can corrupt state the base maintains; it should be private with a protected accessor, or
gone. (3) FileLogger::write(const char*) hides the base's write(const std::string&),
so fl.write(std::string{...}) fails to compile — Module 6's rule returning; a using
Logger::write; or a differently named function fixes it. (4) NullLogger::record and
FileLogger::rotate are extra operations not in the contract, so a caller holding
Logger& cannot use them and must dynamic_cast — which is the interface telling you it is
missing something. And the free function takes const std::vector<Logger> by value: an
abstract type cannot be instantiated, so it does not compile, and if the base were concrete
every element would be sliced. Full credit: all four, plus the observation that (3) and (4)
are opposite failures — one hides a contract function, the other adds an operation outside
it.
Marking yourself¶
| Score | What it means |
|---|---|
| ≥ 80%, every miss explained | Move on to the next module |
| 60–79% | Re-read the sections behind the misses, then retake in a week |
| < 60% | Redo the worked examples and the lab before going further |
A quiz is diagnostic, not a grade. The useful output is the list of concepts that need another look. For this module that list is usually when to split an interface and the destructor definition, because both are decided long before the symptom appears.
Ready to move on when¶
- [ ] You scored ≥ 80%, and any miss has a written explanation, not a shrug
- [ ] Question 7 was answered with the two locations, not just the two mistakes
- [ ] You can write the four contract lines for any function of your own design
- [ ] You have run the contract audit on an interface of yours and changed something as a result
- [ ] You can say which of your types should not be abstracted, and why
- [ ] The skills checklist on the module page is ticked with evidence
Next: Summary — the module compressed to what you must retain. · All modules