Module 11 — Lab 11: every resource owned by exactly one thing¶
Levels: [L3 Modify → L7 Refactor] · Extends: Lab 10 · Time: 3–5 hours
The scenario¶
Lab 10 left you with a catalogue that owns its storage correctly, and a sentence in your own notes that says who owns what. This lab turns that sentence into declarations.
Your library system now opens a resource of its own while it works — an activity report file, or a
lock that stops two imports running at once — and it has grown a Loan record that refers to the
item being lent. Three ownership questions are therefore unanswered in the code, even though your
Lab 9 diagram answered all three on paper:
- Who owns the items the catalogue holds?
- Who releases the report file if the import fails half-way through?
- Does a
Loankeep its item alive, or merely refer to it?
You are going to make the compiler answer all three — and then prove, with counters, that nothing is released twice and nothing is left behind, on the normal path and on the path where something throws.
What you start from¶
Your Lab 10 program, plus this resource wrapper, written the way most people write their first one:
class ReportFile { // opens in the constructor, closes in the destructor
public:
explicit ReportFile(const std::string& path) : file_(std::fopen(path.c_str(), "w")) {}
~ReportFile() { std::fclose(file_); } // ← what if file_ is null? and can this be copied?
void write(const std::string& line) { std::fputs(line.c_str(), file_); }
private:
std::FILE* file_;
};
Read it before you type it. It has one defect that will corrupt a file, and one that can leak a handle. Neither shows up on the happy path.
Requirements¶
-
All ownership expressed in types. Every label on your Lab 9 ownership diagram must now appear in a declaration:
unique_ptrfor exclusive ownership,shared_ptronly where ownership is genuinely shared,weak_ptror a reference for observation, and a value member where a value will do. (Interacts with Module 9: this lab is that diagram, compiled.) -
One RAII wrapper for a non-memory resource, with acquisition in the constructor and release in the destructor. Fix the two defects in the starter class above, and say what each would have caused.
-
Counters proving acquisition and release balance on the normal path. Print the acquisition count and the release count at the end of a full scripted run; they must be equal and the resources still live must be zero.
-
The same counters on a path where an exception is thrown mid-operation. This is the deliverable that matters. The throw is introduced here only as a mechanism — Module 13 is where exception handling is studied properly — but a failure part-way through an import is a realistic scenario, and the counter after the
catchis the evidence that the release was automatic rather than remembered. -
A
weak_ptrback-reference where it genuinely prevents a cycle, with the absence of the cycle demonstrated by destructor output. If you conclude that no cycle exists in your design, say so and demonstrate the back-reference's purpose withlock()succeeding and then failing after the owner dies. -
A polymorphic container holding base-owning pointers, with the virtual destructor present, that counts its elements' destructions to prove every element was deleted exactly once.
-
A written account of one place where
shared_ptrwas rejected, and why. If you usedshared_ptrnowhere, say which place tempted you and what the argument against it was. -
It must build with no warnings using the course's compile command:
What to deliver¶
- The converted system, with no hand-written special member functions in the ownership path.
- The ownership-to-declaration mapping: one line per diagram label, naming the declaration that implements it.
- Two counter runs: the normal one, and the one that throws and is caught.
- The paragraph rejecting
shared_ptrsomewhere.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ] Both scripted runs — the normal one and the throwing one — produce the behaviour asked for
It is correct
- [ ] Acquisitions equal releases on both runs, with nothing still live
- [ ] Each element of the polymorphic container is destroyed exactly once
- [ ] Boundary cases work: empty catalogue, one item, a loan whose item is removed, a report file that cannot be opened
- [ ] Failure cases are handled, not ignored: the acquisition that fails, and the operation that throws
It is designed
- [ ] I can say who owns every resource, and the type says the same thing
- [ ] No hand-written destructor remains except in the RAII wrapper, where it is the point
- [ ] Every ownership-diagram label from Lab 9 appears in a declaration
- [ ] I have one written paragraph rejecting
shared_ptrsomewhere
It is verified
- [ ] The counters were predicted before the first run, and compared afterwards
- [ ] I ran the throwing path deliberately, and caught the exception at a point where I could still print the counts
- [ ] I know which tool — if any — would have reported my bugs, and which of them it would have missed (the cycle and the two-control-block mistake are in the second category)
It is explained
- [ ] I can explain the two defects in the starter
ReportFileand what each would have caused - [ ] I can explain why the leak on the throwing path could not have been fixed by adding a
catch
Common ways this lab goes wrong¶
| Symptom | Cause |
|---|---|
| It compiles, and nothing is ever destroyed | A cycle: a Loan holds its item with shared_ptr and the item holds its loans back the same way |
error: use of deleted function 'unique_ptr(const unique_ptr&)' in the catalogue |
An accessor returns the container by value, or an operation takes it by value. Return a const reference or iterate instead |
| The report file is empty or truncated when the import fails | The wrapper's destructor closed a file that was already closed, or the file handle was copied |
| The counts balance but the exit code is not zero | An exception escaped: check that the throwing path is caught where you intended to print the counts |
The conversion produced std::move everywhere |
Parameters were left as owning ones. Change the callee to T& or const T& and the moves disappear |
| The polymorphic container leaks one kind only | The base destructor is not virtual — Module 7's rule, load-bearing again |
| The lab takes twice as long as the estimate | You are rewriting the catalogue. Reuse Lab 10's file and change the declarations; that is what the lab measures |
Reference solution¶
No solution file is published for this lab. It is your Lab 10 program with its
hand-written lifetime management replaced by types, and the choices are the work: what
owns what, which pointer expresses that ownership, and where shared_ptr is the wrong
answer. A published file would replace the choice with a reading exercise.
The model code for each choice is in examples/module-11/, one program per idea,
each already walked through on the concepts page where it is used:
| Program | The choice it models |
|---|---|
01-raii-across-exit-paths |
RAII measured on all three ways out of a function |
02-raw-memory-bugs-made-visible |
The three memory bugs, made countable instead of fatal |
03-unique-ownership |
std::unique_ptr: exclusive ownership, enforced by the compiler |
04-shared-and-weak |
Shared ownership, and the one bug it creates |
05-custom-deleters |
Resources that are not memory, and the deleters that release them |
Compare after your own version builds and runs — the comparison is where the learning is, and it is worthless before you have something to compare.
Next: Challenge problems — three open problems, including judging
shared_ptr-everywhere against unique_ptr-and-borrow. ·
Previous: Lab 10 ·
Then: Lab 12 — one repository type, several record kinds.