Skip to content

Solutions and answers

Model answers for every exercise, keyed by its number. For a design, refactor, solid or pattern exercise this is one defensible answer, not the only one — what is marked is whether yours is justified.

Every command-line output and every compiler diagnostic quoted below was captured from a real run or a real compile on the course's own toolchain (g++ -std=c++17 -Wall -Wextra -Werror), not remembered. Where a result is implementation-defined, the answer says so instead of pretending otherwise.

Read a solution only after your own answer is written down.

Topic 1 — Mindset and compilation

E01. (1) missing semicolon → compilation. (2) missing #include <iostream> for std::cout → compilation (the name is undeclared). (3) declared-but-undefined function → linking (the compiler emits a call; nothing defines the symbol). (4) null dereference at run time → run time; no compiler stage can catch it because whether a pointer is null depends on data only known while the program runs.

E02. Output:

10
11

bumpByValue(int n) gets a copy, so a is unchanged; bumpByRef(int& n) refers to a itself, so a becomes 11. (The two cannot share the name bump: calling bump(a) with an lvalue is ambiguous between bump(int) and bump(int&) — the compiler rejects it.)

E03.

double after = before * 0.9;
std::cout << "after:  " << after << '\n';

before is 29.5 and after is 26.55.

E04. Output:

sum: 356
average: 71

sum / 5 is computed in int (both operands are int), giving 71, which is then stored in the double. A teacher would expect 71.2 — the fractional part is lost, not rounded, because it was never computed.

E05. std::cout is declared in <iostream>, which the program does not include. The smallest fix is to add #include <iostream> next to #include <string>. (The real diagnostic, reproduced with your toolchain, is a compilation error naming std::cout as undeclared.)

E06. Add std::string supplier[3] = { ... }; and print it in the loop, e.g. << " from " << supplier[i]. More than one line changed — the declaration, and the print — and the loop now indexes four arrays that must stay the same length forever. That coupling is the warning sign: nothing stops someone reordering one array and not the others.

E07. Output:

3
3.5
3

7 / 2 is integer division, so c is assigned the integer result 3 (widened to double). 7.0 / 2 divides in double, giving 3.5.

E08. At least four fragilities: (a) titles, onLoan and borrowedCount must stay in step — adding a title without an onLoan entry compiles; (b) borrowedCount is maintained by hand in lend() and can drift from onLoan; (c) the magic bound 4 appears in the loops and could disagree with the array size; (d) lend() returns false for three different failures (out of range, already on loan, and — by convention elsewhere — success ambiguity), so callers cannot tell them apart. Nothing in the program detects any of them.

E09. (a) The loop starts at index 1, so it adds 20+30+40+50 = 140 — a total short by the 10 at index 0. (b) Off-by-one at the loop start. (c) Start at i = 0, total 150.

E10. A defensible model: Member (identity + name; responsibility "represent one member"), Event (title, date; "represent one event"), and Attendance — or a link kept by the club — recording who attended which event. One thing deliberately not a type: a Name, or a Date if std::string/std::chrono already suffices. Justify the exclusion.

E11. The parallel arrays would need a membership link (who × which event), so a fifth and sixth array or an encoded pair — and the question "how many events did this member attend?" then requires scanning every pair. Introduce Member, Event and a Registration (or the club holding a std::map<Member, std::vector<Event>>). The risk: existing menu code indexes the arrays by position and would change shape. The other side: keep the arrays and add a countEventsFor(memberId) function — rejected because it makes the overhead of the scan grow with every new question, and it does not remove the underlying coupling between the arrays.


Topic 2 — Classes and encapsulation

E12. An invariant is a statement about an object that must hold for its entire lifetime. Two more for a bank account: the account number never changes after construction; the balance equals the sum of all transactions. The first is enforced at construction (make the field const), the second by the one operation that applies a transaction.

E13.

double price() const { return price_; }

void apply_discount(double factor) {
    if (factor > 0.0 && factor <= 1.0) price_ *= factor;
}

Prints Notebook: 3.

E14. Output 5 6. Members are initialised in declaration order: a_ first with 5, then b_ with a_ + 1 = 6 — even though the list names b_ first. GCC's -Wreorder warning (in -Wall) tells the author the list is misleading; the warning is corrected by writing the list in declaration order.

E15. report receives a const Sensor&, and celsius() is not declared const, so calling it would be a promise violation. Fix: double celsius() const { return celsius_; }. An accessor that only reads should always be const — it costs nothing and lets the object be used through a const reference.

E16. Validate in the constructor. One defensible form:

Slot(int hour, int minutes) : hour_(hour), minutes_(minutes) {
    if (hour < 0 || hour > 23 || minutes < 15 || minutes > 120)
        throw std::invalid_argument("invalid slot");
}

An exception is right because a constructor cannot return a status and has no valid object to signal through; the caller cannot accidentally ignore it. A flag would admit an invalid object into the program.

E17. Make the members private, provide id() and name() accessors, and a method that records a return — e.g. void returnLoan() { if (loansOut_ > 0) --loansOut_; } — so the count can never go below zero from outside.

E18. Output:

1
0
70

withdraw(30) succeeds (1) and leaves 70; withdraw(500) fails (0); deposit(-10) is ignored. The line if (amount <= 0.0 || amount > balance_) return false; is the single guard that keeps the balance non-negative.

E19. Lines (1) and (2) return T& and are compile-time leaks: a caller can write session.minutes() = -99; or session.user() = "admin";, defeating any invariant. Lines (3) and (4) return by value and are fine. Line (5) is a legitimate mutator. So (1) and (2) are the only true leaks; (3)'s copy is merely a possible cost, not a leak.

E20. Prints -87 (5 + 8 − 100). The invariant is that a member's strength is positive. Repair: return const std::vector<int>& from members() so callers can read but not modify. If callers genuinely need a filtered view, return a copy instead.

E21. Output:

2
3

show takes const Counter& to promise it will not modify the counter, so it may call only const members. If it took Counter&, it could call increment() and would silently change the caller's object.

E22. A workable design: a Member owning at most one borrowed item, or a Desk that holds the pairing. The cleanest is to let the item know its holder and the member know their item, with borrow/returnItem on the desk enforcing the rule. One concrete shape:

class Item { std::string number_, title_; /* holder set by the desk */ };
class Member {
public:
    bool canBorrow() const { return borrowed_ == nullptr; }
private:
    Item* borrowed_ = nullptr;   // observes, does not own
};

borrow(member, item) succeeds only if member.canBorrow() and item is free; the desk sets both links. The invariant "a member holds at most one item" is enforced in that one operation.

E23. Responsibilities inside Library: catalogue storage, member storage, loan rules, opening hours, fee calculation, and menu display. Split into Catalogue (items), MemberRegistry (members), LendingDesk (loans and rules), OpeningHours, FeeSchedule, and a thin Library (or an application layer) that coordinates them and owns the menu. Dependencies point from the coordinator to the parts. What stays in Library: the small amount of genuine coordination (looking up a member and an item to start a loan). Hardest to change last: the loan rules, because they are entangled with the current fee logic.


Topic 3 — Constructors and lifecycle

E24. (a) A constructor brings an object into a valid initial state. (b) A destructor releases whatever the object owns. (c) A default constructor builds an object with no arguments. If construction fails (throws), no destructor runs, because no complete object ever existed — the fully-constructed members' destructors run, but not the object's own.

E25. Output:

engine on
car on
car off
engine off

Members are constructed before the Car body (so the engine is ready first), and destroyed after it, in reverse — the mirror image.

E26. Output (the first six lines in order):

register T1
register T2
register T3
remove T3
remove T2
register T4

Just before d is constructed, only a (T1) is alive — one sensor.

E27.

Ticket(std::string event, int seat) : event_(std::move(event)), seat_(seat) {}
Ticket(std::string event) : Ticket(std::move(event), 0) {}

The second delegates to the first. It prints Hamlet 12 then Hamlet 0. Delegation avoids repeating the member-initialisation logic.

E28. value_ is assigned in the body rather than initialised in the list, so it is default-initialised (indeterminate for int) before being assigned. Fix:

Reading(int value) : value_(value), unit_("celsius") {}

Now every member is initialised before the body runs.

E29.

~Logger() { delete[] data_; }

The destructor is the right place because it runs on every exit path, including early returns and thrown exceptions, whereas an explicit close() is only called if every caller remembers — and one that forgets leaks.

E30. Output:

make 1
make 2
make 3
drop 3
drop 2
make 4
drop 4
drop 1

c is dropped before b because objects in a block are destroyed in reverse order of construction.

E31. Two differences: Part's body first default-constructs name_ (an empty string) and then assigns the argument, doing two operations where Better constructs it once with the value; and Better moves the argument (std::move) instead of copying it. Better is the better default. It would not matter for a type whose construction and assignment are both cheap and identical, such as int.

E32. close() frees data_, then the destructor frees it again — a double free, which is undefined behaviour (typically crashing at or after the second delete[]). Repair: keep one owner — the destructor frees, and close() only marks the object closed (or nulls data_ and frees for real). The feature deliberately not used yet is a smart pointer (Module 11).

E33. Output:

before
init theme
using theme
after
using theme
destroy theme

s is constructed once, the first time configure() runs; it is destroyed at program exit.

E34. A TempDir guard: the constructor creates the directory, the destructor removes it. Copying must be deleted (two owners would double-remove); moving may be allowed so the guard can be handed on. Usage:

{
    TempDir dir{"work"};   // creates ./work
    // ... use dir.path() ...
}                          // removes ./work, however the block exits

It is stronger than cleanup() because destruction is automatic on every exit path, including exceptions.

E35. Give the handle a class whose constructor opens and whose destructor closes. The four exit paths become one scope with a single destructor callsite; an exception between open and end unwinds through the same destructor. The behaviour to keep identical: the handle is closed exactly once, and never used after closing. Prove it by adding an open/close counter and checking it is balanced on the success path and on each failure path.


Topic 4 — Operators and static members

E36. Two rules: an overloaded operator should mean what it means for built-in types, and it should return what the standard library expects (arithmetic yields a value; assignment yields a reference). Overloading && or , disables short-circuit evaluation and sequencing, so leave them alone.

E37.

long long cents() const { return cents_; }

Money operator+(const Money& other) const {
    return Money{ cents_ + other.cents_ };
}

A member is fine here because both operands are Money. A free function taking two const Money& would be equally correct and would allow implicit conversions on either side; for + between two money values either works. Prints 1425.

E38. Output:

(4, 6)
start (1, 2) end

operator<< must return std::ostream& so the next << has a stream on its left; returning void would make std::cout << "start " << a ill-formed.

E39. Output:

100
101 102 103
103

issued_ must be static because it belongs to the class, shared by every Ticket, not to any one object. Removing int Ticket::issued_ = 100; leaves the static without storage, so the program fails to link (undefined reference to Ticket::issued_).

E40. Only operator< is required by std::sort:

bool operator<(const Product& other) const {
    if (name_ != other.name_) return name_ < other.name_;
    return price_ < other.price_;
}

std::sort needs a strict weak ordering, and < provides it; operator== is not needed to sort, which is why adding just < is complete even though it feels thin.

E41. Returning void gives this captured diagnostic on the a = b = c; line:

error: no match for 'operator=' (operand types are 'Buffer' and 'void')

Assignment returns a reference so that b = c is itself an lvalue that a can be assigned from; the standard library relies on this for chaining and for generic code.

E42. Output:

2
3 2

The postfix overload takes a dummy int to distinguish it from prefix. Postfix is usually expensive because it must copy the object before incrementing, so it can return the old value.

E43. A member operator+ makes the left operand *this, so 2 * m would require 2 to be a Money, which it is not — hence the failure. m + 2 is not blocked because a converting constructor would make m + 2 into m + Money{2} if Money were constructible from an int. Writing + as a free function Money operator+(const Money&, const Money&) allows the same conversion on either side and is preferred for symmetric operators.

E44. A comparison used for sorting must be a strict weak ordering in particular a < a must be false, but points_ <= other.points_ makes it true. That violates the requirement and std::sort is then free to do anything (undefined behaviour). On this toolchain it happened to still print 1 2 2 3, which is exactly why the bug is dangerous — undefined behaviour is not obliged to look wrong. Fix:

bool operator<(const Score& other) const { return points_ < other.points_; }

Sorted output: 1 2 2 3.

E45. Store an integer number of cents (long long) so addition and subtraction are exact and there is no rounding. Operators: +, - (member or free), == and < for comparison, *// by an integer scalar, and << for printing as 12.50. Enforce "never negative" by validating in the constructor and in the subtracting operation (throw, or refuse). Deliberately omit * between two Money (money×money is meaningless) and any implicit conversion from double, which would reintroduce rounding. Storing a double would lose cents to binary rounding, which is unacceptable for money.

E46. Define one operator< on Record: compare title, then year, then author, and return false for two fully-equal records (they are equivalent). std::sort and the three screens all call this. A screen that needs a different order (say author-first) passes a comparator lambda that reuses the same fields — the rule still lives in one place. Equal records must compare as equivalent so that sort does not treat them as unordered, which keeps sorting consistent.


Topic 5 — Inheritance

E47. The test: every Employee must be usable wherever the base is expected, and the base must describe what the derived is. An employee has a date of birth, so this fails; the correct relationship is composition (a DateOfBirth member, or a plain Date). Two more tempting-but-wrong examples: Stack inheriting from Vector (a stack is implemented usinga vector — composition), and Circle inheriting from Point just to get an x/y (composition; a circle has a centre).

E48. Output:

Base Derived ~Derived ~Base

Rule one: bases are constructed before derived parts. Rule two: destruction runs in the reverse order — derived first, then base.

E49.

Polygon(int sides, bool regular) : Shape(sides), regular_(regular) {}

Prints 5 1. It must be built in the initialiser list because Shape has no default constructor — the base sub-object has to be constructed before the derived body runs, and the only way to give it an argument is the member-initialiser list. regular_ is a bool member and could be assigned in the body, though initialising it is better style.

E50. Output:

hire Sana
manager Sana
manager leaves
leave Sana

Manager's destructor body runs first (printing "manager leaves"), then the Employee part is destroyed (printing "leave Sana").

E51. Extract an Item base holding id_ and title_, and derive Book and Movie from it, passing the values up:

class Item {
public:
    Item(std::string id, std::string title) : id_(std::move(id)), title_(std::move(title)) {}
    std::string id() const { return id_; }
    std::string title() const { return title_; }
protected:
    std::string id_, title_;
};

Movie adds minutes_. Output is unchanged. The accessors can stay public; the data is protected only if derived classes genuinely need to touch it — exposing the data is a decision, not a default, and it re-opens the invariant door.

E52. Captured diagnostic:

error: no matching function for call to 'Square::describe()'

Declaring describe(int) in Square hides all Base::describe overloads — name lookup stops at Square and never sees Base::describe(). Two fixes: add using Base::describe; inside Square, or call s.Base::describe(); explicitly.

E53. Output:

Base(10) Mid(5) Leaf

If Leaf had no base initialiser, Mid would need a default constructor — without one, the compiler could not build the base sub-object and the code would not compile.

E54. (a) r.id = 7 is ambiguous because Router contains two Device sub-objects, one through Networked and one through Powered. (b) Two Device sub-objects — one per base path. (c) Line (2) compiles because it names which sub-object it means. (d) A shared base is achieved with virtual inheritance (struct Networked : virtual Device), which costs a pointer-to-base (or similar) per object and some construction complexity.

E55. class Car : Engine uses private inheritance (the default for class), so Engine's
public members become inaccessible to main even though Car can use them. Fix: `class Car
public Engine. Private inheritance is correct whenCaris *implemented in terms of*Enginebut is not anEngine` — a substitute for composition when you also need access to protected members.

E56. Output:

make A make B assemble disassemble drop B drop A

The base Part('A') is built before the member small_('B'); then the Assembly body runs. Destruction reverses: body, then members in reverse declaration order (small_), then base.

E57. A Vehicle base holding the registration plate and the daily rate, with Car, Van and Motorbike adding seats, payload and engine size respectively. The base promises "a registration and a rate"; the cost lives with each kind (or is virtual so it can differ later). What must not be in the base: seats, payload or engine size — a base member that fits one kind and not the others forces every other kind to carry a meaningless field, which is the classic inheritance mistake.

E58. The crux is whether the three classes are the shared thing or merely have it. If the shared part is only data and a small method, prefer a has-a member (e.g. an AuditInfo holding createdOn, owner, active and deactivate()) — this avoids inventing an artificial base. Each class replaces its copy of deactivate() with a call that forwards to the member. One copy-paste may hide a genuine difference (a class whose deactivate() had an extra step) — preserve it by overriding or special-casing. Verify by running the same inputs before and after and comparing output.


Topic 6 — Polymorphism and interfaces

E59. (a) virtual makes the function to call decided at run time from the object's real type. (b) A reference or pointer can point at a derived object, so the object's type is known at run time; a value has already been copied into a base object, so the derived part is gone. (c) An abstract class has incomplete behaviour, so it can name an interface (a parameter or return type), but no complete object of it can exist as a variable.

E60. Output:

base
derived
derived

greet() is not virtual, so ref.greet() uses the static type Base — hence "base"; hello() is virtual, so it uses the object's real type Derived — hence "derived". d.greet() knows the type is Derived.

E61.

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Square : public Shape {
public:
    explicit Square(double side) : side_(side) {}
    double area() const override { return side_ * side_; }
private:
    double side_;
};

Prints 9. override makes the compiler reject the code if area() does not actually override a virtual base function — it catches a typo in the signature.

E62. Printed total: 19 (10 + 9). Each area() dispatches to the element's real type through the Shape*, and the loop uses only the Shape interface, which is precisely what lets it work for kinds it has never seen.

E63.

class Triangle : public Shape {
public:
    Triangle(double base, double height) : b_(base), h_(height) {}
    double area() const override { return b_ * h_ / 2.0; }
private:
    double b_, h_;
};

Nothing in the loop, Shape or the other shapes changes. The property is that the code depends on the interface, not the concrete types — the Open/Closed Principle.

E64. The phenomenon is object slicing: std::vector<Animal> stores Animal by value, so pushing a Dog copies only the Animal part; speak() then calls Animal::speak. Fix: store pointers, std::vector<std::unique_ptr<Animal>>, and speak() prints woof. The cost is an indirection and a heap allocation per animal.

E65. In practice only ~Base runs (this toolchain printed ~Base). It is undefined behaviour, not a leak, because deleting a derived object through a base pointer requires the base destructor to be virtual. Fix: virtual ~Base() { ... }, after which the output becomes ~Derived ~Base.

E66. Three reasons: (1) making every function virtual removes the signal that a function is a deliberate customisation point, so callers cannot tell what is safe to override; (2) virtual calls in constructors and destructors do not reach the derived override, so a virtual private helper can behave differently than the author expects — a correctness problem; (3) it has a runtime cost and prevents inlining. Rule: make a function virtual only when derived types are expected to provide their own behaviour.

E67. Captured diagnostic:

error: cannot declare variable 'r' to be of abstract type 'Report'

Report has a pure virtual title() and no definition, so it is abstract; render() having a body does not help, because the type is still incomplete. Fix: do not create a Report directly — use Summary, which implements title(). s.render() then prints Summary.

E68. Output: base. During base-class construction the object's dynamic type is Base, so the virtual call resolves to Base::describe; the Derived part is not yet constructed. Rule: a virtual call in a constructor or destructor never dispatches to a more-derived override.

E69. A workable interface:

class MediaSource {
public:
    virtual ~MediaSource() = default;
    virtual void open()  = 0;   // post: ready to play, or throws
    virtual void play()  = 0;   // pre: open; post: playing
    virtual void pause() = 0;   // pre: playing; post: paused
    virtual void stop()  = 0;   // post: stopped and released
};

Contract: play before open is an error; failure is reported by exception so callers cannot ignore it. Destructor virtual so a source can be deleted through a MediaSource*. Seeking is a ssecond interface (Seekable) so sources that cannot seek are unaffected and callers that do not need it do not depend on it (Interface Segregation).

E70. Introduce a Channel interface with send, format, cost and retry (or the operations that actually vary); one class per channel. The four switches collapse into calls through the interface. A persisted tag maps to a channel at the boundary (a factory). The cost: more types to navigate and the logic is no longer in one readable block. Prove parity by running each channel through the old and new code and comparing the four behaviours.


Topic 7 — Composition and association

E71. Composition: the part's lifetime is bounded by the whole's — it is created and destroyed with it. Aggregation: the part exists independently and outlives the whole. Plain association: the two merely know each other, with no ownership. A car's engine is composition; a car's driver today is aggregation; a car's manufacturer is association.

E72. Output:

make A
make B
machine ready
machine gone
drop B
drop A

Members are constructed in declaration order (first_, then second_) before the body, and destroyed in the reverse order after the body.

E73.

Person(std::string name, Address address) : name_(std::move(name)), address_(std::move(address)) {}
std::string city() const { return address_.city(); }

Prints Zara lives in Lahore. address_ needs no special destructor handling because Address owns std::strings, which manage their own memory (Rule of Zero).

E74. Output:

acquire Dune
discard Dune
acquire Emma
discard Dune
discard Emma
holds 2
discard Dune
discard Emma

Line by line: the temporary Item{"Dune"} is built then destroyed at the end of the first add; the second add builds its temporary, the growing vector destroys the old Dune during reallocation, then the temporary Emma is destroyed. At the end, lib's vector destroys its two elements — in an order the standard does not specify; this toolchain destroyed them front-to-back. Never rely on that order.

E75. Hold a reference (or pointer) to a shared room instead of a copy:

class Reservation {
public:
    explicit Reservation(Room& room) : room_(room) {}
    void book() { room_.occupy(); }
    Room& room_;
};

Now both reservations see the same room and b.room_.occupied() is 1. The lifetime rule: the Room must outlive every Reservation that refers to it; destroying a room while a reservation still refers to it leaves a dangling reference.

E76. temp is a local in session; when session returns, temp is destroyed, so present_ holds a pointer to an object that no longer exists. The bug becomes observable when room.list() dereferences it — reading freed storage (undefined behaviour). Repair: the reading room should observe, not own — store references and require the members to outlive the room, or store owning values so the room keeps them alive. Decide who owns the members and make their lifetime exceed the room's use of them.

E77. Output:

A+ B+ O+ O- B- A-

Members are constructed in declaration order (a_, then b_) before the Owner body, and destroyed in reverse order after it.

E78. (1) composition (deleting the playlist deletes the songs — value members). (2) aggregation (songs live on — the playlist holds references/pointers to a library that owns them). (3) composition of appointments by the doctor, or association if a clinic owns them (depends — justify it). (4) association (a course refers to a lecturer who exists independently — a pointer/reference). (5) association or aggregation (the document refers to a shared spell-checker — a reference; it does not own it).

E79. A Wife would contain a Husband, which contains a Wife, … — no finite size, so it cannot compile. The Wife would need infinite bytes. Break it with a pointer or reference in one direction (association), or with std::unique_ptr for ownership in one direction plus a std::weak_ptr back — choose one and say when each is right: a pointer when neither owns the other, a smart pointer when one owns the other.

E80. Student, Course and Registration. The registry/University owns its Registrations (composition). A Registration refers to a Student and a Course (association; both outlive it), so it holds pointers or references. Capacity lives in the one operation that adds a registration (e.g. Course::enrol), which checks enrolled_ < capacity_ before adding. When a course is cancelled, the registry removes its registrations (they were composed), while the students remain untouched.

E81. Current claims: Booking owns Room (value), Room points to Booking, Hotel holds copies of both — contradictory (the room's pointer may dangle when a copy dies) and circular. Correct lifetimes: Hotel owns rooms; a Booking refers to a room and a guest (neither owned by the booking); nothing owns a booking except the hotel's booking list. Fix: Hotel owns Rooms and Bookings; Booking holds a Room& (and Guest&); remove the back pointer or make it a std::weak_ptr. Prove the crash is gone by adding a small open/close or ownership log and checking every resource is released exactly once. Final shape: rooms owned by the hotel, bookings owned by the hotel, references downward only.


Topic 8 — Copying and resource management

E82. A shallow copy duplicates the pointer (both objects then name the same block); a deep copy duplicates the pointed-to data. The compiler's default copy copies each member, which for a T* member copies the address — correct for a non-owning pointer, fatal for an owning one.

E83. After Buffer b = a; both a and b point at the same array; at the end of main both destructors run delete[] on it. This is undefined behaviour a double free, which the standard does not promise will behave in any particular way (this toolchain aborted with a runtime failure). It is not merely a leak.

E84.

Buffer(const Buffer& other) : n_(other.n_), data_(new int[other.n_]) {
    for (int i = 0; i < n_; ++i) data_[i] = other.data_[i];
}

Prints 42 7 — the copy is independent.

E85. Output:

build R1
copy R1
drop R1
build R2
copy R2
drop R2
size 2
drop R1
drop R2

With reserve(2) there is no reallocation, so the only copies are the two from push_back building the temporary then copying it into the vector. Record declares a destructor, which suppresses the implicit move constructor, so push_back copies rather than moves. (The final line order is implementation-defined.)

E86.

Register(const Register& other) : n_(other.n_), data_(new double[other.n_]) {
    for (int i = 0; i < n_; ++i) data_[i] = other.data_[i];
}

Register& operator=(const Register& other) {
    if (this == &other) return *this;
    double* fresh = new double[other.n_];          // allocate before freeing
    for (int i = 0; i < other.n_; ++i) fresh[i] = other.data_[i];
    delete[] data_;
    data_ = fresh;
    n_ = other.n_;
    return *this;
}

Prints 1.5 9. Self-assignment is checked because a = a (or an alias) would otherwise free the buffer and then read from it.

E87. On a = ref (self-assignment), line (1) delete[] data_ frees the array and line (2) then reads from the freed storage (and the destructor will free it again) — undefined behaviour. Fix either by guarding if (this == &other) return *this; at the top, or by the copy-and-swap idiom, which needs no guard: build a temporary copy, then swap it with *this.

E88. Output:

open 10
move 10
a is -1, b is 10
close 10

The move constructor sets the source's id_ to -1; the destructor only prints close when id_ != -1, so the moved-from object is silent. A moved-from object is valid but unspecified: it is safe to destroy or assign to, but you must not rely on its value.

E89. Version B must write destructor, copy and move operators because it owns raw pointers; Version A needs none. The Rule of Zero: a class that holds only members which already manage their own resources (like std::string and std::vector) needs no hand-written destructor, copy or move — and so cannot double-free.

E90. b = a copies the pointer, so b's original buffer is leaked (nothing frees it) and both objects now own the same block, so their destructors double-free. It is both a leak and a double free. Fix without writing any of the five: replace int* data_ with std::vector<int> data_ so the compiler-generated copy is a deep copy.

E91. (1) copy — duplicate; (2) move — steal and leave the source empty; (3) copy (take a copy, then release the old); (4) move; (5) neither — release. Move operations must be noexcept becausestd::vector` only moves its elements on reallocation when that cannot throw; otherwise it copies them, to keep its strong exception guarantee (if the move throws mid-way, the container would be left inconsistent).

E92. Store std::vector<double>. If you do, you may write no special members (Rule of Zero) — the compiler's copy duplicates the vector, its move empties the source, and its destructor frees. If you store a raw array, write the destructor plus a copy constructor and a copy-assignment operator (Rule of Three), and optionally a move constructor and move-assignment operator (Rule of Five). A moved-from object must be destructible (e.g. null its pointer). The Rule of Zero is preferable because you write less and cannot get it wrong.

E93. Replace the raw array with std::vector<Item>; the five operations then either vanish or are generated correctly, so "Rule of Zero" becomes true. The observable behaviour to preserve is contents/size/copy-independence; test by copying a Catalog, mutating the copy, and checking the original is unchanged. You would still hand-write an operation only for a non-value resource the vector cannot manage (e.g. an external handle).


Topic 9 — RAII and smart pointers

E94. Resource Acquisition Is Initialisation: acquire a resource in a constructor and release it in the destructor, so a stack object's lifetime is the resource's lifetime — and stack unwinding releases it on any exit, including exceptions. Files, mutexes and sockets are resources too, not only memory.

E95. Output: a +x b -x c. -x is printed at the closing brace of the inner block, when p is destroyed; no delete appears because unique_ptr's destructor does it.

E96.

auto raw = std::make_unique<Logger>(7);
std::cout << "working\n";
// no delete

Prints open 7 / working / close 7. make_unique<Logger> returns std::unique_ptr<Logger>, which owns the object and deletes it when the variable goes out of scope.

E97. Output (this toolchain):

on 1 on 2 on 3 | off 1 off 2 off 3 end

clear() destroys each unique_ptr, which deletes its Device. The destruction order of elements in a vector on clear() is not specified by the standard; do not rely on it.

E98. shared_ptr is required because several users share the connection and none owns it alone — it closes when the last reference goes away, which unique_ptr cannot express (it allows one owner only). widget takes the shared_ptr by reference, so no copy is made and use_count() prints 1 here. disconnected is printed at the end of main, when the last shared_ptr (conn) is destroyed.

E99. a->neighbour = b; b->neighbour = a; gives each node a shared_ptr to the other, so each keeps the other alive and neither is ever destroyed. count a 2, count b 2. Fix: make one direction a weak_ptr (say b's neighbour weak). The weak_ptr does not count toward ownership, so when the last real owner (a) goes, the cycle is broken and both are freed.

E100. Output:

alive 42
count 1
gone

weak_ptr does not own and does not affect use_count, but it can lock() to obtain a temporary shared_ptr while the object is still alive. After reset(), the object is gone and lock() returns an empty pointer — which is why you must check the result before using it.

E101. (1) unique_ptr or by value (the library owns its items). (2) shared_ptr (shared ownership, closed when the last user leaves). (3) weak_ptr (observe without keeping alive). (4) by value (a tiny value type; no indirection needed). (5) weak_ptr (a cache entry that can expire and be rebuilt). shared_ptr is a last resort because shared ownership is hard to reason about and easy to turn into a cycle.

E102. After std::move(job), job is empty (a unique_ptr moves to a null state), so job->id dereferences a null pointer — undefined behaviour, though it compiles. Safe patterns: check if (job), or restructure so the moved-from variable is not used again (let the moved-to variable carry the name).

E103. A Session/SessionGuard class:

class SessionGuard {
public:
    SessionGuard()  { /* open session */ }
    ~SessionGuard() { /* close session */ }
    SessionGuard(const SessionGuard&)            = delete;
    SessionGuard& operator=(const SessionGuard&) = delete;
    SessionGuard(SessionGuard&&) noexcept        = default;
    SessionGuard& operator=(SessionGuard&&) noexcept = default;
};

Copying is deleted because two guards would close one session — the "no two jobs" rule. Moving is kept so a job can hand the guard on (the source becomes harmless). The caller derives: { SessionGuard s; work(); }. Stronger than close() because destruction runs on every exit path, including a thrown exception.

E104. A mechanical rule: does this pointer ever delete/delete[] its target? If yes it is the owner (→ unique_ptr). If several places free it and it must live while any uses it, it is shared (→ shared_ptr). If it only reads someone else's object, it is a borrower (→ raw pointer or reference; never delete it). The one genuinely shared subsystem (if any) stays shared_ptr; everything else becomes unique_ptr; borrowers keep raw pointers. Each unique_ptr is destroyed with its owning module.

E105. Output:

writing
closed
after

unique_ptr's second template parameter is the deleter; FileCloser is called with the std::FILE* instead of delete. fopen can fail and return a null pointer, so if (f) guards against using or closing nothing.


Topic 10 — Templates

E106. (a) A function template is a pattern for a function, parameterised by one or more types. (b) It becomes real code when the compiler sees a call with concrete types. (c) Instantiation is generating that concrete version. Three calls with int, double and std::string produce three specialisations of max<T>.

E107. Output:

9
2.5
pear

larger(3, 9) deduces T = int; larger(2.5, 1.5) deduces T = double; and larger<std::string>(...) compares strings lexicographically, so "pear" > "apple". Line M fails: the arguments have different types (int and double), so T cannot be deduced consistently — a template deduction does not perform conversions between the two arguments.

E108.

template <typename T>
T sum(const std::vector<T>& v) {
    T total{};
    for (const T& x : v) total += x;
    return total;
}

Prints 10 then 4. T is int in the first call and double in the second, and T total{} makes the accumulator the right type (a double total for doubles).

E109. Output:

2 3
20 Grace

The compiler generated two types, Registry<int> and Registry<std::string>. Registry<void> is an error because void is not a complete object type, so std::vector<void> is ill-formed.

E110.

template <>
std::string describe<std::string>(const std::string& value) {
    return "text: " + value;
}

Now describe(42) prints value: 42 and describe(std::string("hello")) prints text: hello. A full specialisation is right because the general implementation genuinely cannot work for std::string (there is no std::to_string(std::string)), which is exactly what specialisation is for; if constexpr would compile the impossible branch for every other type.

E111. Each translation unit compiles because the compiler only needs the declaration to compile a call — but it instantiates twice<int> only where it can see the definition, which lives in widget.cpp. The caller's TU never generates twice<int>, so the linker cannot find it. This toolchain reported:

undefined reference to `int twice<int>(int)'

Fix: put the template definition in the header, where every user can see it. (An alternative is an explicit instantiation template int twice<int>(int); in the .cpp.) nm -C widget.o shows the symbol is missing.

E112. Output:

4
50

N is a non-type template parameter: its value is part of the type, so Ring<int, 4> and Ring<int, 8> are different, unrelated types with different storage.

E113. A template is not compiled once; it is instantiated where it is used, so the compiler must see the definition at the point of use — a mere declaration leaves it unable to generate it. On seeing a concrete call, the compiler emits, say, double max<double>(double, double) into that translation unit. To keep the definition in a .cpp, add explicit instantiations of the types you actually need.

E114. Captured diagnostic (read it bottom-up):

error: no match for 'operator<' (operand types are 'const Reading' and 'Reading')

Reading has no operator<, which smallest uses. Add, for example, bool operator<(const Reading& o) const { return value < o.value; }. The template was not wrong — it needs the type to support <, and the last line of the error names exactly that.

E115. One type parameter for the key, one for the value:

template <typename Key, typename Value>
class Store { std::unordered_map<Key, Value> items_; /* add, find, size */ };

std::unordered_map gives average O(1) lookup and, unlike a plain vector of pairs, direct search; it also does not invalidate references to other elements on insertion. Document the requirement: Key must be hashable and equality-comparable. What is deliberately not templated: the ordering/printing policy, which belongs with the caller.

E116. One class template on the element type replaces the three:

template <typename T> class Store { std::vector<T> data_; /* add, size, get */ };

add, size, get and the destructor become one member each; the three classes become Store<int>, Store<double>, Store<std::string>. One operation that cannot be generic (e.g. a std::string-specific search) stays in a small non-template helper. Verify by running each original behaviour through the template and comparing. The cost: longer compiles and harder error messages.


Topic 11 — Exceptions

E117. Two reasons: a constructor cannot return an error code (there is no valid object to return through), and an error code is easy to ignore — an exception cannot be silently skipped. throw transfers control directly to the nearest matching handler, bypassing the normal flow; leaving a scope destroys that scope's local objects (stack unwinding), so resources are released on the way out.

E118. Output:

runtime: boom
after

std::runtime_error is not derived from std::logic_error (they are siblings under std::exception), so the first catch does not match. Swapping the two clauses would not change this result for these types, but putting a base before a derived handler would make the derived one unreachable.

E119.

class AmountError : public std::runtime_error {
public:
    explicit AmountError(const std::string& message) : std::runtime_error(message) {}
};

void deposit(double amount) {
    if (amount <= 0.0) throw AmountError("negative or zero amount");
    balance_ += amount;
}

Prints a rejection line then 50. Deriving from std::runtime_error lets callers catch it as std::exception, use what(), and group it with other standard exceptions.

E120. Output:

begin
inner saw limit exceeded
outer saw limit exceeded
end

The bare throw; rethrows the same exception object, so the outer handler sees the same what(). Replacing it with throw std::runtime_error("wrapped"); would make the outer line read outer saw wrapped (and the inner message would be lost).

E121.

Reservation(std::string name, int seats) : name_(std::move(name)), seats_(seats) {
    if (name_.empty())              throw std::invalid_argument("empty name");
    if (seats_ <= 0)                throw std::invalid_argument("non-positive seats");
}

The check belongs in the constructor because only there can it guarantee no invalid object ever exists. A bool ok() a caller might forget re-admits invalid objects into the program.

E122. Captured diagnostic:

error: 'class std::runtime_error' has no member named 'field'

Catching by value slices the ValidationError down to std::runtime_error, discarding the field_ member, so field() is unavailable. The phenomenon is slicing. Correct form:

catch (const ValidationError& e) { std::cout << e.field() << '\n'; }

E123. When work() throws "work failed", unwinding begins; g's destructor runs and throws "cannot close" during unwinding. Because a destructor is implicitly noexcept, the second throw calls std::terminate and the program aborts — the outer catch never runs. On this toolchain the terminal shows (on stderr):

terminate called after throwing an instance of 'std::runtime_error'
  what():  cannot close

and GCC warns at compile time: 'throw' will always call 'terminate' [-Wterminate]. Rule: a destructor must never throw.

E124. (1) The std::exception& handler catches it — the derived handler is dead code. (2) The out_of_range handler catches it (correct). (3) catch (...) catches everything before the typed handler is reached — also dead code. (4) By value it slices (still catches, but loses the derived data). Rule: order the handlers most-specific-first, and catch by const reference so no slicing and no unnecessary copy.

E125. second (the raw newed Order) leaks because nothing owns it and the throw skips the delete. first is a stack object, so unwinding destroys it and it does not leak. Fix: hold it in a unique_ptr (or by value), so ownership follows the scope. That is more robust than a delete before each throw, because a new throw added later would otherwise leak too.

E126. A base OrderError : std::runtime_error, with UnknownCustomer, OutOfStock, InvalidDiscount and PaymentTimeout deriving from it, each carrying its extra data. Callers can catch all order failures with catch (const OrderError&) and retry any timeout with a single catch (const PaymentTimeout&). One error deliberately not an exception: a validation warning that is expected and handled inline. The contract relied on: what() returns a human-readable message.

E127. Map each code to a type under a base (ParseError, TooLongError, ChecksumError); throw where the function currently returns a code; catch at the boundary that can act. The caller that ignored the code now cannot — the failure propagates. What becomes harder: failures enter the normal control flow, which some callers find awkward (and hot paths may dislike). Prove no call site still depends on the int protocol by removing the return value and letting the compiler find every old use.


Topic 12 — The STL with OOP

E128. A container stores elements; an iterator names a position in a container; an algorithm works on a range given by two iterators. std::sort is a template, so it is instantiated once for int and once for std::string — one algorithm, many element types.

E129. Output:

5 4 3 2 1
2

a > b sorts descending. The lambda captures nothing — it uses only its parameters.

E130.

double total = std::accumulate(orders.begin(), orders.end(), 0.0,
    [](double acc, const Order& o) { return acc + o.amount; });
auto large = std::count_if(orders.begin(), orders.end(),
    [](const Order& o) { return o.amount > 100.0; });

Prints total 410 and large 2. The initial value must be a double (0.0); if it were 0 (int), the accumulator would be int and the fractions would be truncated.

E131. Output:

120
3
bolt:120 nut:300 washer:80

insert does not overwrite an existing key (only operator[] would), so "bolt" stays 120 and the size stays 3. std::map keeps keys sorted, so iteration is alphabetical: bolt, nut, washer.

E132.

auto negatives = std::count_if(readings.begin(), readings.end(),[](int r){ return r < 0; });
std::vector<int> positives;
std::copy_if(readings.begin(), readings.end(), std::back_inserter(positives),
             [](int r){ return r > 0; });

Prints 3 3. Whether the algorithms are clearer than the loops is a judgement call; the honest answer often is "the loop was clearer for the simple case, the algorithm for the general one".

E133. erase invalidates the iterator it was given and returns the iterator to the next element; it++ then skips one, so some elements are missed (a typical run removes fewer than expected). Idiomatic fixes:

it = items.erase(it);                        // use the returned iterator

items.erase(std::remove_if(items.begin(), items.end(),
            [](const std::string& s){ return s == "expired"; }), items.end());

E134. Output:

3
apple mango zebra

A std::set deduplicates (so size 3) and orders by key (alphabetical). Insertion order would need a sequence container, or a container that preserves insertion order.

E135. (1) std::vector (contiguous, append and iterate). (2) std::map or unordered_map, keyed by name. (3) std::unordered_map<int, ...> (average O(1)). (4) std::list (O(1) insert/erase in the middle). (5) std::set<std::string> (unique and ordered). vector is the default because it is contiguous and cache-friendly and beats a list for most real workloads — change it only when a measurement says to.

E136. [&] captures count by reference; count is a local of makeCounter, destroyed when it returns, so the lambda refers to dead storage and the output is undefined. Fix:

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

Capture by value, and mark the lambda mutable so it can change its own copy.

E137. One std::unordered_map<CatalogueNumber, Item*> for fast lookup by number, and one std::map<CatalogueNumber, Item*> (or a sorted view) for the ordered report; the count is maintained as items are lent and returned. A single container is a poor fit because lookup wants hashing, ordering wants comparison, and counting wants a running total. The invariant: the two containers hold the same items, and any add or remove updates both (in one method). Lookup is O(1) average; ordering is O(log n) per operation.

E138. Replace the three loops with std::max_element, std::count_if and a sort with a comparator. Before changing containers, measure: count the comparisons/allocations or time the operations on realistic data. Try std::unordered_map or a sorted vector first if lookup patterns suggest it; keep the original if the measurement does not improve. Do not replace a loop whose intent is clearer as a loop (for example one with non-trivial side effects).


Topic 13 — SOLID and design patterns

E139. Single Responsibility: one reason to change. Open/Closed: open for extension, closed for modification. Liskov Substitution: a subtype usable wherever the base is. Interface Segregation: many small interfaces beat one fat one. Dependency Inversion: depend on abstractions, not on details. Matches: new kind edited into a class → Open/Closed; subclass unusable where the base is expected → Liskov; an interface with methods most implementers skip → Interface Segregation; a class with many unrelated reasons to change → Single Responsibility; a high-level module importing a low-level detail → Dependency Inversion.

E140. Output:

90
50

The context delegates to a policy object and no longer knows which policy it holds — that is the Strategy pattern, and it satisfies Dependency Inversion: the context depends on the Pricing abstraction, not on Member or Clearance.

E141.

std::unique_ptr<Shape> makeShape(const std::string& kind, double size) {
    if (kind == "circle") return std::make_unique<Circle>(size);
    if (kind == "square") return std::make_unique<Square>(size);
    return nullptr;
}

Prints 12.5664 and 9. Returning by value would slice the shape into a Shape; a unique_ptr preserves the derived type and owns it. main no longer needs to know the concrete classes — the factory encapsulates their creation.

E142. Output:

tick 120
dashboard 120
alert high
tick 90
dashboard 90

This is the Observer pattern. Ticker knows only Observer — nothing about Dashboard or Alert. (Note alert prints nothing for 90, because its predicate is > 100.)

E143. Add only:

class FlashSale : public Pricing {
public:
    double price(double base) const override { return base * 0.7; }
};

No existing class changes — the Open/Closed Principle: extend by adding a class, not by editing one.

E144. s is a local Summary; returning &s returns the address of an object destroyed at the end of the function, so r->render() reads freed storage (undefined behaviour — it may print nothing, garbage, or crash). Fix:

std::unique_ptr<Report> makeReport(const std::string& kind) {
    if (kind == "summary") return std::make_unique<Summary>();
    return nullptr;
}

The caller now owns a valid object.

E145. Output:

coffee + milk + milk
3

This is the Decorator pattern: each WithMilk wraps an inner Drink and adds 0.5 and " + milk". Wrapping composes behaviour at run time without a bool hasMilk flag or a subclass for every combination.

E146. It violates the Single Responsibility Principle. Distinct reasons to change: validation rules; tax rules; the database schema; the receipt format; the email wording; the log format. Split into OrderValidator, TaxCalculator, OrderRepository, ReceiptFormatter, Notifier, Logger, with OrderService coordinating them and depending on interfaces. Keep in OrderService: the genuine orchestration of a single order (look up, validate, charge, persist). The symptom a maintainer notices: unrelated changes keep landing in the same file and every change risks the others.

E147. FixedDeposit violates the Liskov Substitution Principle. Account promises that withdraw succeeds when funds allow; FixedDeposit cannot honour that promise for any amount, so it cannot be substituted for an Account. Fix: do not make FixedDeposit an Account — model it as a distinct type (a deposit that matures), or share an abstract BalanceHolder base that does not promise withdrawal.

E148. An Observer interface with update(Event), and a hub with subscribe(EventType, Observer&) / unsubscribe(Observer&). Unsubscription should be safe (e.g. hand back a token, or mark-and-sweep) so unsubscribing is safe even if it happens during a notification. Ownership: the hub does not own observers — the caller does — so a destroyed observer must unsubscribe first; a weak_ptr or a documented rule makes that explicit. A Factory can create observers by event type. The design is judged on the unsubscription and ownership rules.

E149. A ShippingStrategy interface with cost(weight), and one class per method:

class ShippingStrategy { public: virtual ~ShippingStrategy() = default;
    virtual double cost(double weight) const = 0; };

Look the strategy up from a std::map<std::string, std::unique_ptr<ShippingStrategy>> rather than an if-chain. An unknown method throws (returning -1.0 silently produced a wrong price). The shared base fee or rounding lives in one helper. Adding a fifth method is a new class and a new map entry — no existing class is edited.


Topic 14 — Refactoring and design

E150. Refactoring changes the structure of code without changing its behaviour. It cannot begin safely until there is a way to detect a behaviour change — tests, or at least captured output. "Rewrite from scratch" usually changes behaviour, so it is a rewrite, not a refactoring.

E151. Output:

-5:-1 0:0 7:1 42:2 100:3 999:3

This is the characterisation test: it records the current behaviour so the refactor can be checked against it. You write it first because otherwise you cannot tell a deliberate change from an accidental one.

E152.

double circleArea(double radius) { return 3.14159 * radius * radius; }
// ...
std::cout << circleArea(3.0) << '\n';   // 28.2743
std::cout << circleArea(5.0) << '\n';   // 78.5397

(The six-significant-digit default of std::ostream rounds these.) The two benefits: the formula lives in one place, and a change to pi or the formula affects every caller. One disadvantage: an extra function call, negligible to the compiler here.

E153. Extract printHeader, printRow and printFooter (or printReport). One of them should become a class if it needs to remember state between rows (a running total, a column width) or has more than one cooperating responsibility. The signal: a function needs a parameter just to carry state from one call to the next — that state belongs in an object.

E154. The two printed values are 0 and 7. best = 0 is wrong for the all-negative vector (no element exceeds 0), while the mixed vector still has positives so 7 wins — the bug hides. The lesson: when extracting behaviour from a loop, preserve the initial value exactly (values[0]). A characterisation test on the negative case would have caught it at once.

E155. A 300-line method → long method: split it into named steps. A bool isProcessed flag consulted in nine places → boolean-as-state: replace with a small state enum or two types. A bool mode parameter changing behaviour in three branches → flag argument: split into two methods. A temp used only inside one method but declared as a member → should be local: move it into the method. The last one is not worth fixing on its own if the class is otherwise stable; fix the long method first.

E156. If clamp() ran before addRaw, the second add(70) would clamp before adding, so the total would reach 130 instead of 100 — a behaviour change, not a refactoring. Rule: extracting a method must keep the statements in the same order and with the same effect; reordering is a behaviour change and must be justified and tested separately.

E157. Layers: a domain layer (Equipment, Member, Loan, Maintenance), an application layer (coordinators that run use-cases), and an infrastructure layer (file persistence, console). Dependencies point inward: infrastructure depends on the domain, never the reverse. Persistence lives behind a repository interface declared in the domain, implemented in infrastructure, so the domain types never know the file format. The interfaces (EquipmentRepository, Clock) let a module be tested with a fake. Build the persistence last, because it is the most disposable detail. Testability is the yardstick.

E158. (1) A Manager passed everywhere → a god object / hidden coupling (violates Single Responsibility and Dependency Inversion); the risk is that a change to Manager ripples through every class. Fix: pass the specific collaborators each class needs. (2) save() writing SQL in a domain class → Dependency Inversion / Single Responsibility violation; the risk is the domain cannot be tested without a database. Fix: move persistence behind a repository. (3) An interface with eleven methods and seven empty → Interface Segregation violation; the risk is that implementers lie about what they support and callers cannot tell. Fix: split into small interfaces. Something it got right: it grouped the domain ideas into named types rather than parallel arrays. A question to ask: which of these classes is actually used in more than one screen? Change the thing that has a consumer.

E159. First write characterisation tests that lock the current console output for a set of inputs. Introduce one type — say Item — and move the first array into it, changing as little as possible. Repeat one array at a time, compiling and running the tests after each step. The riskiest step is moving lend, because it touches several arrays; do it where you can revert with a single commit and where the tests cover it. Leave the menu-display code alone: it is not part of the domain and rewriting it adds risk for no design benefit.

E160. Model answer structure (using E157's design): one sentence per type; for each relationship say whether it is composition (owns and dies together), aggregation (refers to an independently-owned object) or association (mere reference); name the hardest principle — usually Dependency Inversion for the persistence boundary; state your least-certain decision honestly (for example, whether Maintenance should be its own type or a flag on Equipment); and describe one rejected alternative (for example, a single Workshop god class) and why. It is judged on (4) and (5): a designer who claims no uncertainty has not examined the work.


Bank home: The Exercise Bank · Hints: hints.md · Exercises: topic pages