Skip to content

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 Loan keep 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

  1. All ownership expressed in types. Every label on your Lab 9 ownership diagram must now appear in a declaration: unique_ptr for exclusive ownership, shared_ptr only where ownership is genuinely shared, weak_ptr or a reference for observation, and a value member where a value will do. (Interacts with Module 9: this lab is that diagram, compiled.)

  2. 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.

  3. 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.

  4. 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 catch is the evidence that the release was automatic rather than remembered.

  5. A weak_ptr back-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 with lock() succeeding and then failing after the owner dies.

  6. 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.

  7. A written account of one place where shared_ptr was rejected, and why. If you used shared_ptr nowhere, say which place tempted you and what the argument against it was.

  8. It must build with no warnings using the course's compile command:

g++ -std=c++17 -Wall -Wextra -Werror main.cpp -o library

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_ptr somewhere.

Self-assessment checklist

It builds and runs

  • [ ] Compiles with g++ -std=c++17 -Wall -Wextra -Werror with 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_ptr somewhere

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 ReportFile and 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.