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:
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.
before is 29.5 and after is 26.55.
E04. Output:
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:
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:
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:
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:
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):
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:
Now every member is initialised before the body runs.
E29.
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:
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:
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:
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:
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:
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:
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:
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:
Rule one: bases are constructed before derived parts. Rule two: destruction runs in the reverse order — derived first, then base.
E49.
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:
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:
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:
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 : Engineuses private inheritance (the default forclass), soEngine's publicmembers become inaccessible tomaineven thoughCarcan 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:
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:
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:
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:
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:
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:
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:
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:
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.
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):
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:
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:
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:
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:
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:
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:
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):
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:
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:
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:
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:
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:
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):
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:
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:
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:
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:
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:
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:
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:
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:
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