Skip to content

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.

square area 9
square area 9

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