Skip to content

Final Self-Assessment — Answers

Back to the questions

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.

engine on
car on
car off
engine off

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.

count 1
gone

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.

base

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.

1 5
2

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.

a-1 b3
close3 

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:

return [count]() mutable { return ++count; };

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 <:

bool operator<(const Item& o) const { return value_ < o.value_; }

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:

  1. Unsubscription during dispatch. publish must not be iterating the container unsubscribe mutates. 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.
  2. 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_ptr and skip expired ones in publish. Do not hold shared_ptr on both sides: mutual shared_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.