Final Self-Assessment — Answers¶
Every output quoted below was captured by compiling and running the snippet on the course
toolchain (g++ -std=c++17 -Wall -Wextra -Werror). Each answer says why, because the
reason is the thing worth learning.
Part A — Recall¶
Q1 — B. Preprocess, compile, link. (The linker is where a missing definition is noticed.)
Q2 — False. A missing declaration is a compiler error; a missing definition is a
linker error — undefined reference to 'foo()'.
Q3 — C. Returning a mutable reference to internal data hands out the object's state without passing the operations that protect its invariant. Private members are the mechanism of encapsulation, not the test for it.
Q4 — A, B, C, D. Owning a raw buffer means writing the destructor, copy constructor,
copy assignment and move constructor (and, for completeness, move assignment) — the Rule of
Five. toString() has nothing to do with resource management.
Q5 — B. std::shared_ptr: shared ownership, destroyed when the last owner leaves.
Q6 — False. The definition must be visible where the template is used, so it belongs
in the header (or an included .tpp); otherwise you get an undefined-reference link error.
Q7 — A, B, C. Basic, strong, nothrow. "Deferred" is not a guarantee.
Q8 — C. std::map keeps keys sorted with O(log n) lookup. std::unordered_map is
O(1) average but unordered.
Q9 — B. Liskov Substitution.
Q10. Every function, class, variable or template has exactly one definition across the
whole program — one definition, while declarations may be repeated. (The single most
important consequence for this course: a non-inline function defined in a header included
by two .cpp files gives multiple definition of ... at link time.)
Part B — Prediction¶
Q11.
The member engine_ is constructed before the Car constructor body runs, so
engine on precedes car on. Destruction is the exact reverse: the class destructor body
prints car off, then the member is destroyed and prints engine off.
Q12.
The vector stores Animal values. zoo.push_back(Dog{}) copies the Dog into an
Animal slot, slicing away the derived part, and stores an Animal. So speak() resolves
to Animal::speak. The fix is indirection: std::vector<std::unique_ptr<Animal>>, pushing
std::make_unique<Dog>().
Q13.
wp is a weak_ptr, which does not own: it does not raise use_count, so the count is
still 1 (the shared_ptr alone). After sp.reset() the last owner is gone and the object
is destroyed, so wp.lock() returns an empty shared_ptr and the test prints gone.
Q14.
A virtual call made from a base constructor does not dispatch to the derived override,
because during Base::Base the Derived part does not yet exist. The dynamic type seen
inside the base constructor is Base. (This is why you should not call virtual functions
from constructors.)
Q15.
After std::sort the vector is {1, 2, 3, 4, 5}, so front() is 1 and back() is 5.
Even numbers are 2 and 4, so the predicate is true twice and n is 2.
Q16.
The move constructor runs on std::move(a), so b takes id 3 and a is left as -1
(the moved-from marker). Printing gives a-1 b3. At the end of main, b is destroyed
first (reverse construction order) and prints close3; a's id is -1, so its destructor
prints nothing. Note the trailing space after close3, and that it is not followed by a
newline.
Part C — Debugging and analysis¶
Q17 — a non-virtual destructor, deleting through the base pointer.
delete p calls Base::~Base only; Derived's destructor never runs, so anything
Derived owned leaks. If the layouts differ this is outright undefined behaviour. Fix:
virtual ~Base() {}. The rule to carry away: a class with any virtual function, used
polymorphically, needs a virtual destructor.
Q18 — a dangling reference in a lambda capture.
[&] captures count by reference, but count is a local of makeCounter and dies
when it returns. The returned closure holds a reference to dead stack memory; calling it is
undefined behaviour. Fix — capture by value, and make the closure mutable so it can change
its own copy:
Q19 — name hiding.
Derived::show(int) hides every Base::show overload, so d.show() finds no
zero-argument candidate. Fix: using Base::show; inside Derived (or call
d.Base::show();). Compile-time, not run-time.
Q20 — the comparison is not a strict weak ordering.
return a.value() <= b.value(); makes compare(a, a) true, so the irreflexive half of
strict weak ordering is violated. std::sort relies on that ordering to partition and
merge; with a broken one it can step past the end of the range, read outside the vector, or
corrupt the sequence — undefined behaviour, not merely a wrong sort order. It bites only for
inputs that contain equal values, which is why it survives casual testing. Fix: use <:
The same function underpins std::map, std::set, std::sort and std::binary_search,
so the fix applies to all of them at once.
Q21 — write none of them (the Rule of Zero).
Every member (std::string, std::vector) already manages its own resource and implements
its own copy and move correctly. The compiler-generated destructor, copy and move
operations do the right thing: they forward to the members. Writing them by hand adds code
whose only possible effect is to introduce a bug. Write one only when the class must own
something the members do not express — a raw pointer, a file handle, a lock — and then write
all of them (or, better, wrap that resource in one member class and return to the Rule of
Zero).
Part D — Design¶
Q22 — a MediaSource interface.
class MediaSource {
public:
virtual ~MediaSource() = default; // required: it is deleted through this interface
virtual void open(const std::string& uri) = 0;
virtual void play() = 0;
virtual void pause() = 0;
virtual void stop() = 0;
};
The failure rule every implementation must respect: the calls must be usable in any order
without crashing — in particular, pause, stop and a repeated open are legal and must
be idempotent, or must report why they cannot proceed by throwing a typed exception. A
MediaError hierarchy (NotFound, Unsupported, DeviceBusy) makes the cause catchable.
The interface stays narrow (four operations, no format or codec details) so implementations
are substitutable and test doubles are trivial.
Q23 — an observer hub.
class Observer {
public:
virtual ~Observer() = default;
virtual void onEvent(const Event&) = 0; // one method: narrow and implementable
};
class Hub {
public:
void subscribe(Observer& o);
void unsubscribe(Observer& o);
void publish(const Event& e) const;
};
Two safety rules:
- Unsubscription during dispatch.
publishmust not be iterating the containerunsubscribemutates. Either dispatch over a copy of the observer list, or collect removals and apply them after the loop. Doing neither is how you get a crash the first time a subscriber removes itself in response to an event. - Ownership. The hub does not own its observers. A raw reference/pointer expresses
that, but the observers must then outlive their subscription — document it. If the
observers are heap objects that may die first, hold
std::weak_ptrand skip expired ones inpublish. Do not holdshared_ptron both sides: mutualshared_ptrs leak.
Q24 — refactoring the conditional.
Replace the function with a small hierarchy behind one interface — a ShippingPolicy with a
single Money cost(const Order&) const operation, one class per method (Standard,
Express, Pickup, Freight), and a Checkout that holds one policy and delegates. The
if disappears entirely.
To add a fifth method a caller writes one new class implementing cost and passes it in.
Nothing existing is opened, edited or recompiled. That is open/closed, and it is also why the
change is now testable on its own: each policy can be unit-tested with no I/O and no other
policies present. The trade-off to state: for two or three methods that never change, a
switch is genuinely simpler; this refactor earns its cost precisely because the set grows.
Q25 — layers and dependency direction.
From outside in:
presentation (menus, prompts, console or GUI) depends on ↓
application (use cases: borrow, return, maintain) depends on ↓
domain (the types and the rules: Equipment, Member, Loan)
ports (interfaces the domain needs: Store, Clock) ← owned by the domain
adapters (FileStore, SystemClock) depend on the ports
The dependency direction is always inward: the domain knows nothing about a console or a
file. It declares the interfaces it needs (Store, Clock) and the concrete classes
implement them — dependency inversion. That single rule buys both testability (substitute an
in-memory Store and a fixed Clock, and the rules become deterministic and disk-free) and
substitutability.
Persistence belongs at the edge, in an adapter implementing Store. The file format —
delimiters, a version header, parsing — lives entirely inside FileStore and never leaks
into the domain; a format change is one file, and the domain does not recompile because its
model changed. Build it last: writing the parser first freezes the model around the file
format, whereas shaping the model from the rules and adding the parser behind the interface
afterwards keeps the design honest.
Scoring. 25 questions. Score in three bands: Part A answers are right or wrong (10); Part B is all-or-nothing per line (6 + 6 lines); Parts C and D are judged — a defect named with a correct fix, or a design that is justified and names a trade-off. Use Marking Yourself to turn the total into a decision: 90–100% ready for advanced work, 75–89% review weak areas, 60–74% practice required, below 60% revisit the module.