Module 7 — 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. Name the two conditions that must both hold for a call to be dynamically bound, and give one case where a call on an object is statically bound even though the function is virtual.
(Answer: the function must be virtual, and the call must go through a reference or
pointer to a base class. A call on a named object is statically bound because the
compiler knows the exact type — plainBike.dailyRate() returns 60000 whether or not the
function is virtual. Full credit: the phrase the static type of the expression
decides, because it is the sentence that explains both halves.)
2. What exactly does override do, and what does it not do?
(Answer: it asks the compiler to verify that the function really overrides a base
function, and fails the build otherwise; it generates no code and costs nothing at run
time. It does not make the function virtual (the base already did), and it does not
make the override extend the base's behaviour — an override replaces it. Full credit: a
statement that a signature mismatch without override produces a new function that
hides the base's, which is the mistake the keyword exists to catch.)
3. When does a class need a virtual destructor, and what happens without one?
(Answer: any class with virtual functions that may be destroyed through a base pointer.
Without one, only the base destructor runs, the derived part is not destroyed, and the
standard makes it undefined behaviour. Full credit: naming virtual ~Base() = default;
as the default habit, and at least one of the three safe alternatives — a protected
non-virtual destructor, or not owning raw resources at all (Module 11).)
4. What is object slicing, and name three places it happens.
(Answer: a copy constructs an object of the static type, so a derived object copied into
a base object loses everything the derived class added, and dispatch has nothing left to
dispatch to. Three places: a by-value parameter, a container of base objects, and
assignment to a base object (Base b = derived;). Full credit: the observation that the
copy is correct — exactly what the code asked for — which is why no compiler reports
it.)
5. Explain, in one sentence each, why a virtual call during construction and during destruction resolves to the class currently being constructed or destroyed.
(Answer: during construction the derived members do not exist yet, so dispatching to a derived override would read uninitialised state — the object's dynamic type is the class whose constructor is running. During destruction the derived part is already gone, so the same argument holds in reverse. Full credit: adding that if the function is pure virtual, the call is undefined behaviour rather than merely wrong.)
Part B — Prediction and explanation¶
6. What does this print, and why?
class Base {
public:
virtual ~Base() = default;
virtual std::string tag() const { return "base"; }
};
class Derived : public Base {
public:
std::string tag() const { return "derived"; }
};
void show(const Base& b) { std::cout << b.tag() << ' '; }
int main() {
Derived d;
show(d);
Base b = d;
show(b);
std::cout << sizeof(Derived);
}
Answer.
The first show receives a reference, so dispatch runs Derived::tag(). The second
receives b, which is a Base object — Base b = d; sliced it, and b.tag() is a
Base::tag() call on a Base (static binding, since the expression is a named object
anyway). sizeof(Derived) is this compiler's answer for a class with one hidden vtable
pointer: 8 on a 64-bit build. Full credit requires derived base plus the word
slicing plus the observation that Derived::tag has no override — it is virtual
anyway, because the base's is, which is a fact worth knowing and not a mistake here.
7. This program compiles and prints 200000 twice. Name both defects, and say which
one you would fix first.
class Vehicle {
public:
virtual ~Vehicle() = default;
long long rate() const { return 200000; }
};
class Motorbike : public Vehicle {
public:
long long rate() const { return 60000; }
};
int main() {
std::vector<Vehicle> fleet;
fleet.push_back(Motorbike{});
std::cout << fleet[0].rate() << ' ';
Motorbike bike;
fleet.push_back(bike);
std::cout << fleet[0].rate() << ' ' << fleet[1].rate();
}
Answer. The first defect is the missing virtual on Vehicle::rate, so a call
through a Vehicle expression binds statically. The second is the container of
objects: both push_back calls slice, so fleet[0] and fleet[1] are Vehicle
objects with no Motorbike part left. Fix them together — each alone leaves a bug — and
the fix is virtual long long rate() const plus
std::vector<std::unique_ptr<Vehicle>>, with fleet.push_back(std::make_unique<Motorbike>()).
Full credit: naming both, stating that fixing one alone is insufficient, and predicting
that sizeof would have shown the slice before any output was printed.
8. What does this print, and what does the second line tell you?
class Base {
public:
Base() { std::cout << "ctor: " << name() << '\n'; }
virtual ~Base() = default;
virtual std::string name() const { return "base"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "ctor: " << name() << '\n'; }
std::string name() const override { return "derived"; }
};
int main() { Derived d; std::cout << d.name() << '\n'; }
Answer.
During Base()'s body the object's dynamic type is Base, so the base's name() runs;
by the time Derived()'s body runs, the derived part exists and the override is
reachable. Full credit: saying why the first line is not a bug — dispatching to
Derived::name() there would be reading an object that is not finished — and noting that
the same rule applies to ~Base() if the destructor called a virtual function.
9. A colleague proposes making every function of every class virtual, "so we can
extend anything later". Give two concrete costs, one of which is measurable in one line
of code.
Answer. First cost: every polymorphic object carries a hidden pointer — measurable
as sizeof(Circle) versus sizeof(TaggedShape) in
examples/module-07/06-switch-to-dispatch/main.cpp, 16 bytes versus 24 for the tagged
struct, and 8 bytes per object for a base with no data at all. Second cost: a class with
a vtable can no longer be freely copied, stored by value, sorted by value, or constexpr
— and its objects slice. Third, worth a mark: every call becomes an indirection, which
cannot be inlined in the general case. Full credit: at least two costs with one of them
backed by a number, and the observation that "so we can extend later" is a guess about
the future that every other reader of the interface now pays for.
10. Here is a hierarchy with four defects. Find them all.
class Logger {
public:
~Logger() = default;
void log(const std::string& line) { write("[" + level_ + "] " + line); }
virtual void write(const std::string& line) = 0;
std::string level_ = "info";
};
class FileLogger : public Logger {
public:
void Write(const std::string& line) override {}
void log(const char* line) { write(line); }
};
void run(std::vector<Logger> loggers) {
for (Logger logger : loggers) logger.log("start");
}
Answer. (i) ~Logger() is not virtual while the class has virtual functions and an
abstract contract — delete through a Logger* is undefined behaviour. (ii)
FileLogger::Write capitalises the name and uses override, so it does not override
anything: a compile error, and without override it would silently hide write and leave
the class abstract. (iii) FileLogger::log(const char*) hides the base's
log(const std::string&), so fl.log("x") is fine but fl.log(std::string{"x"}) is not
— the Module 6 rule returning; the base's log should be reachable, and a using
Logger::log; or a different name fixes it. (iv) The free function takes and iterates
std::vector<Logger> by value: an abstract class cannot be instantiated, so this does
not compile at all — and if Logger were concrete, every element would be sliced. The
fix is const std::vector<std::unique_ptr<Logger>>&. Full credit: all four, with the
observation that (ii) and (iii) are the same underlying rule (hiding versus overriding)
seen from two directions.
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 slicing and the destructor, because both are silent until something is measured.
Ready to move on when¶
- [ ] You scored ≥ 80%, and any miss has a written explanation, not a shrug
- [ ] Question 7 was answered by naming both defects, not by fixing one
- [ ] You can state the four-answer trace of question 8 from the rule
- [ ] You have reproduced a slice on purpose and fixed it, and kept the outputs
- [ ] You can say what
sizeofandtypeid(x).name()each tell you, and which is misleading after a slice - [ ] The skills checklist on the module page is ticked with evidence
Next: Summary — the module compressed to what you must retain. · All modules