Module 13 — Lab 13: failure as part of the design¶
Levels: [L4 Analyze → L6 Build] · Extends: Lab 12 · Time: 3–4 hours
The scenario¶
The library system now has one Repository<T>, smart pointers, and a catalogue that survives being
copied. It still assumes that everything works: a title that is not held returns nothing distinguishable
from an empty catalogue, a malformed file crashes, and a loan that exceeds the member's limit is
silently recorded anyway.
The campus IT office has started running the program against real files, and their bug report is one line long: "it stops without saying why, and twice we had to restart the machine."
You are going to make failure part of the design. Every operation that can fail gets a named, documented failure; every operation's exception-safety guarantee gets written down; and the failure paths get run and counted, not described. That last part is the deliverable a marker would look at first, and it is the part most students skip.
What you start from¶
Your Lab 12 program. The three operations that need work look like this — each is correct on the success path, which is exactly why the bugs have survived:
class Catalogue {
public:
bool add(const Item& item); // false = could not add. Which failure? Unknown
std::shared_ptr<Item> find(int id) const; // nullptr = absent. Or was it an error?
void loadFrom(const std::string& path); // aborts on a malformed file
private:
std::vector<std::shared_ptr<Item>> items_;
};
class LoanLedger {
public:
void lend(std::shared_ptr<Item> item, const Member& member);
// Records the loan even when member.overdueCount() >= Member::limit,
// because nothing in the current design can refuse.
};
Requirements¶
- A custom exception hierarchy rooted in
std::exception, with at least three distinct failures thrown from real operations — for exampleItemUnavailable(carrying the title),MemberNotFound(carrying the id) andCorruptCatalogue(carrying the file and the line). Eachwhat()names the object involved. (Interacts with Module 8: the hierarchy is itself a small interface design, and a caller must be able to catch it broadly or narrowly.) - Every operation's guarantee is documented in the header, one line each, in the form
// Strong guarantee: on failure, the catalogue is unchanged.— no operation left unclassified. - At least one operation is upgraded to the strong guarantee, and the comment names the technique (work on a copy, then swap) and why the last step cannot fail. (Interacts with Module 10: this is copy-and-swap, used as a guarantee.)
- No
catchblock releases a resource. Ownership stays in types. If you find yourself writing acatchthat callsclose()orreset(), the resource was in the wrong place. (Interacts with Module 11.) - Counters proving balance on the success path and on a path where an exception is thrown mid-operation. This is the requirement that makes the lab real, and it is specified below.
- A malformed-input case handled without crashing: a file with one bad line is reported with the file name and the line number, the program continues, and the catalogue's state is unchanged.
- A destructor review: one line per class stating why its destructor cannot throw, and what happens to a failure it does discover.
- No operation throws from a destructor, and every operation that cannot fail is marked
noexcept— checked with astatic_assert, not asserted in a comment. - It builds with no warnings using the course's compile command.
The two runs (requirement 5 in detail)¶
This is the core of the lab. You run the same scripted sequence twice:
Run A - everything succeeds
load the catalogue from a good file
lend three items, returning two of them
print the counters
Run B - one operation fails half-way
load the catalogue from a good file
start a lend that will fail (member at the limit, or an item already out)
catch the failure, report it, continue
return the items, save, print the counters
And the counters must balance in both:
Run A: acquisitions 14 releases 14 live 0
Run B: acquisitions 13 releases 13 live 0 (and 1 refusal, which acquired nothing)
An operation that fails after acquiring something is where a leak would show, so Run B must fail at the worst possible moment — not before the acquisitions. Make the failure happen on purpose, at a line you chose:
if (failAtItem == index) {
throw ItemUnavailable("simulated failure at item " + std::to_string(index));
}
If the counters differ between your two runs, the difference is the bug: it names how many acquisitions were not released, and the unwinding trace names which one.
What to deliver¶
- The program, with the hierarchy, the documented guarantees and both runs available from the command
line (
./library --run-aand./library --run-b, or two entry points), each printing its counters. - The transcript of both runs, pasted side by side or one after the other.
- A short table with one row per operation: what it throws, what guarantee it provides, how the failure was tested. An untested claim belongs in the table as a gap, not as a tick.
- One paragraph on the failure you changed from throwing to returning a value, with the
reason — because the right answer is not always an exception, and the module's judgement exercise is
worth more than another
throw.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ] Both runs complete and print their counters
It is correct
- [ ] Boundary cases work: an empty catalogue, a single item, a member at the limit, a missing id
- [ ] The malformed file is reported (file name and line) and the program continues
- [ ] Nothing leaks on the failure path: the counters balance in both runs
- [ ] The strong-guarantee operation leaves the state unchanged when it fails, and this is asserted
It is designed
- [ ] Every operation's guarantee is written next to its declaration
- [ ] The hierarchy is shallow and each level has a reason to exist
- [ ] No
catchblock releases a resource - [ ] Destructors cannot throw, and the one that might fail says what it does instead
It is verified
- [ ] The failure was produced deliberately, at a known line — not awaited
- [ ] I tested the failure path, not only the success path (this module's substance)
- [ ] Diagnostics are on
std::cerr, so an abort cannot swallow them - [ ] I can explain why the second run's counter balance is stronger evidence than the first
Common ways this lab goes wrong¶
| Symptom | Cause |
|---|---|
| The counters balance in Run A and not in Run B | A resource is not owned by an object; look for a raw handle acquired in a constructor body or released by a statement |
The catch block never runs |
A handler is ordered after a base class that already matched, or catch (...) above both |
| The malformed file ends the program | The exception escaped main; there is no boundary handler, or the load path throws something not derived from std::exception |
| The program terminates with a message naming a destructor | A destructor throws. Report, do not throw |
error: looser exception specification on overriding virtual function |
An override throws where the base declared noexcept — the base's contract has to be changed deliberately, and every caller revisited |
warning: 'throw' will always call 'terminate' [-Wterminate] |
A throw inside a noexcept function or a default destructor. Under -Werror this is a stop, which is the point |
| The strong-guarantee operation still mutates on failure | Validation happens after the mutation. Validate first, or work on a copy and swap |
| The state check after the failure is missing | Requirement 3 is a claim about behaviour; without the assertion it is only a comment |
Reference solution¶
No solution file is published for this lab. It is a system whose failure paths must be as considered as its success path, and the decisions are the work: what is refused, what is thrown, what is left unchanged, and where an exception is the wrong report.
The model code is in examples/module-13/, one program per idea, each already walked
through on the concepts page where it is used:
| Program | What it models |
|---|---|
01-error-codes-versus-exceptions |
The same failure reported three ways, and what each forces on the caller |
02-unwinding-and-cleanup |
What an exception does on its way out of a function |
03-custom-hierarchy-and-slicing |
Designing an exception hierarchy, and the three mistakes that make it useless |
04-guarantees-and-noexcept |
The three exception-safety guarantees, and the promise noexcept makes |
05-validation-and-design-rules |
The decision this module builds towards: throw, return, or refuse |
Two things are worth comparing against your own version in particular: the counter output for both runs, which is the evidence that nothing leaked on either path, and any operation where you chose between throwing and returning a value.
Continue¶
Previous: Exercises · Next: Challenge problems · Module home: Module 13