Exercises — 15 problems

← Module home · Attempt each problem in writing before opening its solution — the course protocol. Part A: the machinery. Part B: design and safety.

Part A — the machinery (E1–E8)

Part B — design and safety (E9–E15)


Solutions

(Attempt first. The solutions are the *shape of an answer — yours may differ and still be right.)*

S1. The second call’s output never appears in the same run: the first throw vacates the whole try block, so control jumps to the catch and the program continues after it. To exercise both paths through one try each, the second call needs its own try. Output:

caught: celsius below absolute zero
98.6

The lesson the exercise is built for: one try guards one attempt — a “resume after failure” flow needs either multiple try blocks or a loop around one.

S2. With exception first, both throws land in the exception block — the specific catch blocks below it are unreachable for those types (base-first swallows derived). After swapping, out_of_range goes to the specific desk and runtime_error to the family desk. The compiler accepts both orders silently; only the behaviour changes — which is why hunt D2 exists.

S3. mode == 3 lands in the handler (runtime_error is an exception). Modes 1 and 2 throw things that are not std::exception — int and a string literal — so no handler matches, unwinding reaches main’s top with no reception desk, and std::terminate ends the program. catch (...) is the only net that would have caught them. The design lesson: throw exception-derived types only; the type system is your dispatch table.

S4. Exact output:

born
born
died
died
caught: boom

t2 is born inside levelTwo, so it dies first when the throw unwinds that frame; then t1 dies as levelOne unwinds; then the catch runs. Reverse order of creation — the unwinding rule from Lesson 1’s diagram, now observed rather than stated.

S5. "abc" → caught, rethrown, prints parseAge: bad age string: abc. "99999999999999" → out_of_range (the number doesn’t fit int), rethrown with the same enriched message — the catch both handlers feed. "19" → prints age: 19. The enrichment line is identical for both, which is the point: the caller reports the string, not the internal converter’s vocabulary.

S6.

int readIntInRange(istream& in, int lo, int hi) {
    for (int attempt = 1; attempt <= 3; ++attempt) {
        int x;
        if (in >> x && x >= lo && x <= hi) return x;
        in.clear();
        in.ignore(1000, '\n');
        cout << "(attempt " << attempt << " of 3) Enter a number "
             << lo << "-" << hi << ": ";
    }
    throw runtime_error("readIntInRange: three bad attempts");
}

The guard layer keeps the expected case (one typo, fixed) cheap and quiet; the persistent failure escalates. Boundary validation stays local; escalation is the event.

S7.

double shortfall() const { return requested - available; }

and the handler prints fixed-precision to two decimals. The design point: the handler needed no access to the account, no globals, no parsing — the exception carried its own report. Data-carrying exceptions turn handlers into reporters.

S8. (a) Guard layer: a bad menu number is expected every session, the fix is local (re-prompt), and throwing would make the main loop unreadable. (b) Throw: a mid-session save failure is unexpected, the function cannot fix it (the menu cannot open files), and the application layer must choose between retry, discard, or abort. One sentence each — but the decision was the exercise.

S9.

class BookingError : public runtime_error {
public: explicit BookingError(const string& m) : runtime_error(m) {}
};
class RoomOccupiedError : public BookingError {
public: RoomOccupiedError(const string& room) : BookingError("occupied: " + room), room(room) {}
        const string& getRoom() const { return room; }
private: string room;
};
class InvertedDatesError : public BookingError { /* check-in, check-out carried */ };
class UnknownGuestError  : public BookingError { /* guest id carried */ };

The ladder: catch (const RoomOccupiedError&) first (offer alternatives), then catch (const BookingError&) (report and refuse), then the world net. One type per failure kind — three, not thirty, because the handler’s behaviour really does differ for occupied vs everything else.

S10. The fallible step must come first and the in-memory commit last:

void addToCartAndLog(Cart& cart, LogFile& log, const Item& it) {
    log.append("adding " + it.name());   // may throw — cart untouched
    cart.add(it);                        // commit last (basic-safe)
}

Strong guarantee, by ordering: if the log throws, the cart is as-if-never-called and a retry is safe. (If cart.add could throw after the log write, the order would be wrong the other way — the analysis is always: which step leaves the system lying if it succeeds alone?)

S11. Swallowed species #2 becomes a reported continuation:

try {
    marks = loadMarks(path);
} catch (const exception& e) {
    cerr << "warning: marks unavailable (" << e.what() << ") — continuing empty\n";
    // marks stays empty; every later use is guarded or informed
}

The catch now does one of the three legal things (fix / rethrow / report-and-stop-the-operation). A continued program with an announced empty state is honest; a continued program with a silent empty state is the bug.

S12. The scenario: an exception is already unwinding (say, loadMarks threw) — during unwinding, every live destructor runs; if ~SessionLog throws now, there are two active exceptions and std::terminate fires instantly, hiding the original error. The only correct fix: the destructor catches its own failure and reports it non-throwingly (cerr, a flag, a “flush failed” note) — destructors are the no-fail level. A “critical flush” belongs in an explicit close() the user calls, where throwing is legal.

S13. (a) No guarantee — a throw from push_back leaves v cleared-but-empty: valid memory, but the operation half-happened on the caller’s data with no promise. (b) Strong — all fallible work happens in tmp; the commit v = tmp doesn’t throw for vector<int>; failure leaves v as-if-never-called. (c) No-fail — a destructor doing cleanup only (and delete on a valid owned pointer doesn’t throw). (d) Basic — if log.write throws, the account state is valid (money moved, invariant intact) but the pair is half-updated — no lie, no leak, no strong promise.

S14. Layer sketch: prevent — Student’s constructor throws on age < 5; the Course object cannot exceed capacity (ask-door returns false). guard — the menu’s reader re-prompts on non-numeric input. throw — enroll throws CourseFullError (carrying course code and seats) when a race between check and add still fails, and RegistrationError rethrown with student+course context. net — main’s catch (const exception&) then catch (...), logging and stopping politely. What does not belong: re-prompt loops below the menu (a library function should not interrogate the user), and CourseFullError thrown from the constructor (a constructor builds valid objects; fullness is a use-time condition of the course, not the student).

S15. A complete answer has all four parts. The grade points: the invited failure must name a species (e.g. “species #1 — ifstream in(path) unchecked, missing file prints an empty report”); the robust version’s guard must be at the boundary (empty-vector refusal), the throw typed with context ("loadMarks: cannot open " + path), and the rethrow enriched at a different layer than the catch that finally handles it. If all three tools appear in one function, the layering is wrong — that observation is half the exercise.