Skip to content

Hints

One nudge per exercise, keyed by its number. A hint is meant to point you at the idea or the section to reread — not to give the answer. Read one only after a genuine attempt has stalled, and read the solutions only after your own answer is written down.

If a hint here hands you the answer, it has failed; please say so.


Topic 1 — Mindset and compilation

E01 — Ask who reads the file at each stage: the preprocessor before the compiler does, the compiler per translation unit, the linker once for all of them. Which stages know a name exists but not where it is?

E02 — A parameter int n is a fresh local copy; int& n is another name for the caller's variable. Only one of the two functions can change what main sees.

E03 — A discount of 10% off means multiplying by 0.9, not subtracting 0.1. For the second print, copy the shape of the first.

E04 — Add the five marks by hand, then look very carefully at the types on the line that computes the average. sum is one type; the literal 5 is another.

E05 — A name used with std:: is declared in a header. Which header declares std::cout, and which declares std::string? (Compare with the working examples in examples/module-01/.)

E06 — Add the fourth array, then add it to the loop's output. Count every line you touched — including the loop, not only the array — and ask whether the arrays could ever fall out of step.

E07 — Both operands of 7 / 2 are int, so the division happens in int before the result is stored in the double. Write the value of the division first, then widen.

E08 — Look for the things the compiler cannot check: a count kept by hand, two arrays that must stay the same length, a function that reports two different failures the same way. Name the line for each.

E09 — The loop begins at index 1, not 0. Which element is never added? Compute both the wrong total and the right one.

E10 — Start from the questions the club will ask ("who attended this event?", "which events did this member attend?"). Types exist to answer questions; a "Member" and an "Event" are candidates, but ask whether "Attendance" is a thing in its own right.

E11 — The new question is "how many events did this member attend?" — a question the current arrays cannot answer without scanning everything. Write down which array holds the membership links, and whether that is a type waiting to happen.


Topic 2 — Classes and encapsulation

E12 — An invariant is a statement that must be true of every object, always. For a bank account, ask what must never happen, then find the operation that could cause it.

E13 — price() only reads, so mark it const. apply_discount changes one member; validate the factor before you multiply.

E14 — Members are initialised in declaration order, whatever order the initialiser list is written in. Read the declaration order, not the list order.

E15 — report receives a const Sensor&, so it may only call member functions promised not to change the object. One keyword makes that promise.

E16 — The constructor is the one place that can guarantee a slot is never invalid, so the check belongs there. Choose your failure mechanism and be ready to defend it — an exception is the course's answer from Module 13 onwards.

E17 — Make the data private and decide what a caller genuinely needs: reading the id and name, and recording a return. A setter for loansOut on its own re-opens the hole.

E18 — Trace each call: which returns false, which changes the balance, and what deposit(-10.0) does. The single guard is the one line that stops the balance going negative.

E19 — Functions returning T& (lines 1 and 2) hand out a means to write. Compare them with lines 3 and 4, which return by value.

E20 — members() returns a mutable reference, so the caller can push anything. The invariant is about positive strengths. For the repair, decide what an inspector needs: to read the strengths, not to edit them.

E21 — Both increment() calls happen before the first print, and one more before the second. const Counter& means "I will not modify it" — so which operations may it call?

E22 — One item per member means the member holds at most one item, and the item knows its holder. Decide who owns the link, and make the "at most one" rule impossible to break — a plain borrowedItem_ set by a method that first checks it is empty is a start.

E23 — List the reasons the class changes: items, members, loans, hours, fines, menu display. Each distinct reason is a candidate class. The coordinator is whatever genuinely needs to see more than one of them at once.


Topic 3 — Constructors and lifecycle

E24 — A constructor brings an object into a valid state; a destructor releases what it holds. If construction fails, no object existed — so count the destructor calls.

E25 — Members are constructed before the constructor body runs, and destroyed after it. Member first, body second; and destruction mirrors it.

E26 — Objects in a block are destroyed at the closing brace, in reverse order of construction. Count the live sensors just before d is created.

E27 — A delegating constructor calls another constructor of the same class in its initialiser list. Write the (event, seat) constructor first, then delegate to it.

E28 — One member is assigned in the body instead of initialised in the list. For an int that is not fatal, but for a class-typed member it can be; rewrite the whole initialiser list so both members are initialised, not assigned.

E29 — Whatever a constructor allocates, a destructor must free. Add the destructor. Why is the destructor more reliable than asking every caller to call close()?

E30 — The inner block's two objects die at its closing brace, newest first; the outer objects die at the end of main. Count all of them, including the last one.

E31 — In the initialiser list, a member of class type is built directly with the right value. In the body, it is first default-constructed and then assigned. Which one does more work for std::string?

E32 — close() frees the buffer, then the destructor frees it again. Make ownership single: either the destructor owns it and close() only marks it, or close() nulls the pointer. (Module 11's smart pointers make this automatic — you are not there yet.)

E33 — A function-local static is constructed the first time control reaches it, and destroyed once at program exit. Count the "init" lines across the two calls.

E34 — The object is the directory's lifetime: construct it to create the directory, destroy it to remove it. Forbid copying (two owners) and think about whether moving is useful.

E35 — Every exit path must agree on one cleanup point. If a local object's destructor does the cleanup, then early return and thrown exceptions both reach it automatically. Which behaviour must stay identical, and how would you prove it?


Topic 4 — Operators and static members

E36 — A good overload does what the symbol means to everyone else, and returns the type the standard library expects (+ yields a value; = yields a reference). Overloading && or , breaks short-circuiting or sequencing, so leave them alone.

E37 — operator+ needs no privileged access if it uses a public accessor, so consider a free function that takes two const Money&. Return a new Money built from the sum.

E38 — operator+ returns a fresh Vec2; operator<< must return the stream itself so another << can follow it. Predict a and b first, then the additions.

E39 — issued_ must be shared by every Ticket, so it is static. Look at what the constructor does to it and what issuedCount() reports before and after the three tickets are made. Without the out-of-class definition the linker fails — the static has no storage.

E40 — std::sort needs only operator< (and operator== is not required). Write a strict comparison: name first; if the names are equal, compare price.

E41 — Assignment must yield an lvalue so a = b = c can chain: b = c must produce somerthing assignable toa. Returningvoid` breaks exactly that.

E42 — Prefix increments and returns *this. Postfix takes an unused int to mark it apart; it must return the old value, so it makes a copy first. Track n_ step by step.

E43 — For a member operator+, the left operand is always *this, so 2 + m has no way to make 2 a Money. A free function taking (const Money&, const Money&) lets the compiler convert either side — and it is symmetric.

E44 — A comparison used for sorting must be a strict weak ordering: a < a must be false. <= makes it true, which is undefined behaviour in std::sort. Fix it to < and list the scores ascending.

E45 — Store the amount as an integer number of cents, so arithmetic is exact. Provide only the operators the desk actually uses, and enforce "never negative" where an amount enters the type (the constructor and the subtracting operation).

E46 — One operator< defines the order: compare title, then year, then author. A screen needing a different order should pass a comparator that uses the same fields, not a second hand-written comparison.


Topic 5 — Inheritance

E47 — Inheritance is for "is-a": every Derived must be usable wherever the Base is. An employee has a date of birth, so that is composition, not inheritance.

E48 — Bases are constructed before derived parts, and destroyed after them. The destructor bodies run inside-out relative to the constructors.

E49 — Shape has no default constructor, so a base sub-object must be built in the initialiser list before the derived constructor body runs. regular_ is a plain member and could be handled either way.

E50 — Employee is constructed first (printing "hire"), then Manager's body. On destruction the derived body runs first. Order every line.

E51 — Put id_ and title_ in a base such as Item, have Book and Movie derive from it, and pass the base values up in the initialiser list. Protected access is a decision: it lets derived classes reach the data, which you may or may not want.

E52 — Declaring describe(int) in Square hides every describe in Base — the name lookup stops at the derived class. using Base::describe; brings the other overload back into view; or call s.Base::describe() explicitly.

E53 — The base initialiser runs before each derived body, innermost base first. Mid doubles its argument before passing it up; the question about a missing base initialiser is about what Base would then need.

E54 — Two base classes both derive from Device, so a Router has two Device sub-objects — name them with Networked:: or Powered::. The C++ feature that shares one base is virtual inheritance.

E55 — class Car : Engine is private inheritance: the public members of Engine become inaccessible to main, though Car itself may use them. Writing : public Engine fixes it. Private inheritance is the right choice when the relationship is "implemented in terms of", not "is-a".

E56 — A base is built before members; members are built in declaration order. So Part('A') (base) runs before small_('B'). Destruction is the exact reverse.

E57 — Put only what every vehicle has (plate, rate) in the base. Seats, payload and engine size belong to their own kinds — a base member that fits one kind is the classic mistake here.

E58 — Ask whether the three classes genuinely are the shared thing (inheritance) or merely have it (composition). If one class's deactivate() legitimately differs, the duplication was hiding a real difference.


Topic 6 — Polymorphism and interfaces

E59 — virtual makes the call decide at run time from the object's type; a call on a value has the static type baked in. An abstract class has no complete object, so it names an interface, not a variable.

E60 — greet() is not virtual, so the call uses the static type (const Base&). hello() is virtual, so it uses the object's real type. d.greet() knows the type is Derived.

E61 — Make area() pure virtual (= 0), give Shape a virtual destructor (= default), and mark Square::area with override so a mismatch is a compile error.

E62 — Each area() dispatches to the element's real type. Compute 2*5 and 3*3, add them. The loop uses only Shape, which is the point.

E63 — A new Triangle : Shape needs no change to the loop or the other shapes; that is what programming to the interface buys.

E64 — std::vector<Animal> stores Animal by value, so the Dog part is sliced away. Store pointers — std::vector<std::unique_ptr<Animal>> — so the derived part survives. The cost is an extra indirection and heap allocation.

E65 — Deleting a Derived through a Base* whose destructor is not virtual is undefined behaviour; in practice only ~Base runs. Make it virtual ~Base().

E66 — virtual costs an indirection and, more importantly, tells readers a function is a customisation point. Making everything virtual removes that signal and can change behaviour (calls in constructors, fragile base classes). Reserve it for functions a derived type is expected to replace.

E67 — Report has a pure virtual title(), so it has no complete implementation and is abstract; a variable cannot be of an abstract type. You cannot "fix" it by giving render() a body — implement title() in a derived class and use that.

E68 — During base construction the object's dynamic type is Base, so the virtual call resolves to Base::describe. Never call a virtual function expecting derived behaviour from a constructor or destructor.

E69 — Give the interface four pure virtual operations and write the contract for each (preconditions, postconditions, failure behaviour). A source that can also seek should implement a second, separate interface so callers that do not need seeking are unaffected.

E70 — Move each channel's send/format/cost/retry into a class implementing one interface. The enum disappears except where a tag is persisted (map it at the boundary). Name the trade-off — more types to navigate — and how you would prove parity.


Topic 7 — Composition and association

E71 — Composition: the part dies with the whole (an engine in a car). Aggregation: the part outlives the whole (today's driver). Association: they merely know each other (manufacturer).

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

E73 — Initialise address_ from the parameter in the list, and have city() forward to address_.city(). Address needs no special handling because it owns a std::string, which manages itself.

E74 — Watch for the temporary Item in each add (created then destroyed) and the reallocation on the second push_back. The final destruction order of the two stored items is implementation-defined; note that rather than assuming.

E75 — Sharing a room means holding a reference or pointer instead of a copy, so both reservations see the same room. State who must outlive whom: the room must outlive every reservation that refers to it.

E76 — temp is destroyed when session returns, so present_ holds a dangling pointer; room.list() then reads an object that no longer exists. The reading room should not own the members — the caller should, and they should outlive the room's use of them.

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

E78 — Decide, for each pair, whether the part can outlive the whole. A spell-checker is usually shared or owned elsewhere, so a Document typically refers to it, not owns it.

E79 — Value members need complete types, and each would contain the other, so neither can be sized. Break it with a pointer (or a reference) in one direction, or a std::unique_ptr/std::weak_ptr.

E80 — The registry owns its registrations (composition). Student and Course outlive a registration, so a registration refers to them. The capacity check lives in the one operation that adds a registration.

E81 — Write down every "owns" claim, find the cycle and the contradictions, then assign one owner per resource. For proof without a debugger, add lifetime logging or an opens/closes counter.


Topic 8 — Copying and resource management

E82 — Shallow copies the pointer; deep copies the pointed-to data. The compiler's default copies each member, which for a T* copies the address — wrong when the object owns it.

E83 — After Buffer b = a, both destructors delete[] the same block. Which of "leak", "double free" and "undefined behaviour" the standard actually promises is the key distinction.

E84 — The copy constructor allocates its own buffer of other.n_ and copies each element. Do not copy the pointer.

E85 — reserve(2) prevents reallocation, so the only copies come from push_back copying the temporary. Note that Record declares a destructor, which suppresses the implicit move constructor — so it copies.

E86 — Add operator= that allocates first or reuses safely, and guard this == &other. Doing the allocation before freeing the old buffer is one way to be exception-safe.

E87 — When this == &other, line 1 frees the buffer that line 2 then reads from. Guard with if (this == &other) return *this;, or use the copy-and-swap idiom, which needs no guard.

E88 — Only the destructor of the object with a real id prints close; the moved-from object has id -1. A moved-from object is valid but unspecified — you may destroy it or assign to it, not rely on its value.

E89 — Version B writes all five operations because it holds raw pointers; Version A needs none, because std::string and std::vector enforce the Rule of Zero for it.

E90 — The compiler's copy assignment copies the pointer, leaking b's original buffer and leaving two objects sharing one. Replacing the raw pointer with a std::vector<int> removes the problem without writing any special member.

E91 — Copy makes an independent duplicate; move takes the resource and leaves the source empty; the destructor releases. Move operations must be noexcept because std::vector prefers moves on reallocation only when they cannot throw (else it copies to keep the strong guarantee).

E92 — Write as few of the five as correctness allows. A move leaves the source owning nothing; its destructor must still be safe. If std::vector<double> holds the marks, you may be able to write none of them.

E93 — Replace the raw array with a std::vector, and each hand-written operation either disappears or is generated correctly. Test the observable behaviour (contents, sizes, copy independence) before and after.


Topic 9 — RAII and smart pointers

E94 — Resource Acquisition Is Initialisation: acquire in the constructor, release in the destructor, so stack unwinding cleans up on any exit. Files, locks and sockets are resources too, not only memory.

E95 — The unique_ptr lives for the inner block; its destructor runs at the closing brace. Find which brace produces -x.

E96 — Replace Logger* raw = new Logger{7}; with auto raw = std::make_unique<Logger>(7); and delete the manual cleanup. The pointer owns the object; leaving the scope frees it.

E97 — clear() destroys each unique_ptr, which deletes its Device. The order of element destruction in a vector is not specified by the standard — note that rather than assuming.

E98 — shared_ptr is required because several users share ownership and none owns it alone. The widget parameter is a reference, so no copy is made — check what that does to use_count().

E99 — a->neighbour = b; b->neighbour = a; keeps both alive. Make one direction a weak_ptr (an ownership-free link) to break the cycle.

E100 — weak_ptr does not affect use_count. After reset(), the object is gone, so lock() returns an empty shared_ptr.

E101 — Own ⇒ unique_ptr; shared and co-owned ⇒ shared_ptr; observe without owning ⇒ weak_ptr; a small value type ⇒ by value. shared_ptr is a last resort because shared ownership is hard to reason about and easy to cycle.

E102 — After std::move, job is empty; job->id dereferences a null unique_ptr, which is undefined behaviour. Check the pointer, or restructure so the moved-from name is not used.

E103 — The guard owns the session for its lifetime. Forbid copy (a second owner would double-close), allow move (so a job can hand it on). The move must leave the source harmless.

E104 — Classify each pointer as owner (→ unique_ptr), shared (→ shared_ptr), or borrower (→ raw pointer or reference, never deleting). Say where each owner is destroyed.

E105 — The deleter is the second template parameter; unique_ptr calls FileCloser{} on the FILE* instead of delete. fopen may return nullptr, so check before use.


Topic 10 — Templates

E106 — A template is a pattern the compiler fills in per type; instantiation is that filling-in, at compile time. Three calls with three types ⇒ three specialisations.

E107 — Deduce T from the arguments. For a std::string, > compares lexicographically. larger(3, 9.5) makes the two argument types differ, so T cannot be deduced consistently.

E108 — Template on the element type, take the container by const& (or on Container and use typename Container::value_type). Start the accumulator at zero of the right type.

E109 — The compiler generates a separate Registry<int> and Registry<std::string>. void is not a complete object type, so std::vector<void> is ill-formed.

E110 — A full specialisation for std::string gives a different body; choose it because the general body genuinely cannot work for std::string, which is what specialisation is for.

E111 — The compiler only instantiates a template when it sees a use and the definition. Living in a .cpp the caller cannot see, twice<int> is never generated, so the linker cannot find it. Put the definition in the header.

E112 — N is a non-type parameter: part of the type, so Ring<int,4> and Ring<int,8> are different types. Print capacity and the sum of the two stored values.

E113 — Templates are instantiated, not compiled once, so the definition must be visible where used. The workaround that keeps the definition in a .cpp is an explicit instantiation of the types you actually need.

E114 — The last line of the error names the real problem: Reading has no operator<. Add a comparison (by value, say) — the template itself is fine.

E115 — One type parameter for the value, one for the key. A std::map<Key,Value> (or std::unordered_map) fits lookup; document the requirement the key must satisfy.

E116 — One template on the element type replaces the three classes. If one operation cannot be generic, keep it in a thin wrapper. State the cost: longer compile times and harder-to-read error messages.


Topic 11 — Exceptions

E117 — An error code cannot be returned from a constructor, and it is easy to ignore; an exception cannot be ignored silently and unwinds the stack correctly. Leaving a scope by exception destroys the scope's local objects.

E118 — std::runtime_error is not a std::logic_error (they are siblings under std::exception), so the first catch does not match and the second does. Swapping puts an unrelated catch first, which changes nothing here — but a base before a derived would.

E119 — Derive AmountError from std::runtime_error, forwarding the message, and throw it when amount <= 0. Deriving from std::runtime_error lets callers catch it as std::exception and use what().

E120 — The inner catch handles out_of_range, prints, then throw; rethrows the same object to the outer std::exception handler. Replacing it with a new throw would change the what() text.

E121 — Validate in the constructor and throw std::invalid_argument; a constructor cannot return a status, so an exception is the only way it can refuse.

E122 — Catching by value slices the ValidationError down to std::runtime_error, so the field() member is gone. Catch by const ValidationError& (or at least by reference).

E123 — The Guard destructor runs during unwinding and throws a second exception; because a destructor is implicitly noexcept, that calls std::terminate. GCC warns with -Wterminate. Never let a destructor throw.

E124 — Handlers are tried in order; a base class before a derived one makes the derived handler unreachable (and is usually a warning/error). Put the most specific first, and catch by const reference. catch (...) before a typed handler swallows everything.

E125 — first is a stack object, destroyed during unwinding, so it does not leak; second is a raw new and is not owned by anything, so it leaks when the throw skips delete. Hold it in a unique_ptr (or a value) so ownership follows the scope.

E126 — A common base (e.g. OrderError : std::runtime_error) lets callers catch all order failures at once; each concrete type carries its extra data. Make the timeout a distinct subtype so "retry any timeout" is one catch clause.

E127 — Map each code to a type deriving from a base. Throw where the code was returned, catch at the boundary that can handle it. The caller that ignored the code now cannot ignore the failure. The cost is that failures become part of the control flow, which some APIs dislike.


Topic 12 — The STL with OOP

E128 — Containers hold data, iterators name positions, algorithms do the work through iterators. std::sort is a template, so it is instantiated per element type.

E129 — a > b sorts descending. The lambda captures nothing — it uses only its parameters. Count the even numbers in the sorted vector.

E130 — std::accumulate(begin, end, 0.0, lambda) — the initial value's type decides the accumulator's type, so use 0.0 for a double total. std::count_if with a predicate.

E131 — insert does not overwrite an existing key; operator[] would. std::map keeps keys sorted, so iteration is alphabetical.

E132 — std::count_if for the negatives, std::copy_if with std::back_inserter for the positives. Then judge readability honestly.

E133 — erase invalidates the iterator you are holding and returns the next one, so ++it skips an element. Use the returned iterator, or std::remove_if then erase.

E134 — A std::set deduplicates and orders by key, so size() is 3 and iteration is alphabetical. Insertion order needs a sequence container or a different index.

E135 — Append-and-iterate ⇒ vector. Search by name ⇒ map/unordered_map. Integer-id cache ⇒ unordered_map. Frequent middle insert/erase ⇒ list. Sorted unique names ⇒ set. vector is the default because it is contiguous and cache-friendly.

E136 — [&] captures count by reference; count is destroyed when makeCounter returns, so the lambda refers to dead storage. Capture by value and mark the lambda mutable so it can increment its own copy.

E137 — One container for lookup (unordered_map by id), one for ordered iteration (map, or sort on demand), and keep them consistent in one place. The invariant is that both hold the same items.

E138 — Replace each loop with the matching algorithm; for the container question, measure first (count or time the operations) rather than guess, and keep the original if the data says so.


Topic 13 — SOLID and design patterns

E139 — S: one reason to change. O: extend without editing. L: a subtype usable where the base is. I: narrow interfaces. D: depend on abstractions. Match each symptom to the principle it violates.

E140 — Member::price gives 100*0.9, Clearance::price gives 100*0.5. The context delegates to a policy object; it no longer needs to know which policy it holds (Dependency Inversion / Strategy).

E141 — Return std::unique_ptr<Shape>, branching on kind. Returning by value would slice. main no longer needs to know the concrete classes (the factory hides them).

E142 — setPrice prints "tick", then each observer updates in the order attached. Ticker knows only Observer — nothing about Dashboard or Alert.

E143 — Add a FlashSale : Pricing class. No existing class changes — that is the Open/ Closed Principle: open for extension, closed for modification.

E144 — s is a local Summary, so returning &s returns the address of a destroyed object. Return std::unique_ptr<Report> built with std::make_unique<Summary>().

E145 — Each WithMilk appends " + milk" and adds 0.5 to the inner cost. Two wrappers ⇒ two appends and +1.0. It is the Decorator pattern; wrapping composes behaviour without a flag or a combinatorial set of subclasses.

E146 — Split validation, tax, persistence, formatting, notification and logging into separate classes; OrderService coordinates and depends on abstractions. A method that has to be edited for unrelated reasons is the symptom in the wild.

E147 — FixedDeposit is an Account, so it must honour withdraw's contract; throwing always breaks it (Liskov). Model a fixed deposit as something you cannot withdraw from — a different type, or an account that permits no withdrawal by design.

E148 — Observer interface with an update; a register that returns a token or handles used to unregister safely; ownership of observers by the hub or a documented external owner. Combine with a factory if events are created by type.

E149 — A ShippingStrategy per method, looked up from a std::map<std::string, std::unique_ptr<ShippingStrategy>> (or a factory). An unknown method should throw, not return -1.0. The shared part (base handling fee, or rounding) lives in one place.


Topic 14 — Refactoring and design

E150 — Refactoring changes structure without changing behaviour, so it needs a way to tell behaviour is unchanged — tests, or at least captured output.

E151 — Follow the branches for each input. That captured output is your safety net for the refactor: you must reproduce it exactly.

E152 — Extract circleArea(double), call it twice, keep the printed values identical. A single formula means pi changes in one place.

E153 — Extract three named functions first. A function becomes a class when it needs to remember state between calls, or has more than one cooperating responsibility.

E154 — best = 0 fails for an all-negative vector; values[0] was the original. The mixed case still works by luck. A characterisation test on the negative case catches it.

E155 — Long method, flag argument, boolean-as-state, and a member that should be local. Name the harm and the smallest fix for each; one is cosmetic and can wait.

E156 — If clamp() ran before addRaw, the second add(70) would not clamp the total to 100, printing 130 instead of 100. Order is part of behaviour, so extraction must preserve it.

E157 — Domain types know nothing about files; a repository interface in the domain, a file-format implementation outside it. Dependencies point inward (toward the domain). Build the persistence last.

E158 — God object, persistence in the domain, and a fat interface with empty implementations. Say the concrete risk for each, and find something the design got right.

E159 — Write characterisation tests first; introduce one type (e.g. an Item) and move one array into it; keep every step green; do the riskiest change where you can revert it easily; leave one thing alone deliberately.

E160 — Answer each part precisely; the last two (your least-certain decision and the alternative you rejected) are the ones being assessed.