Module 11 — Exercises¶
Every exercise states its cognitive level, its task, and the observable result that counts as success. Work in a scratch directory and compile with the course's flags, treating a warning as a failure:
Warm-up [L1 Understand]¶
W1 — Eight declarations, eight ownership answers¶
Task. For each declaration, say what owns what, and whether the code as written leaks. Then say what you would change.
// (a)
class A { std::vector<Book> books_; };
// (b)
class B { std::vector<Book*> books_; Book* allocate(const std::string& title); };
// (c)
class C { std::unique_ptr<Book> current_; };
// (d)
class D { Book* current_ = nullptr; }; // current_ points at an element of books_
// (e)
class E {
public:
~E() { delete book_; }
private:
Book* book_;
}; // copy constructor: not declared
// (f)
class F { std::shared_ptr<Book> book_; };
// (g)
class G { std::shared_ptr<Book> book_; G* parent_ = nullptr; };
// (h)
class H { std::weak_ptr<Book> book_; };
Success looks like. Eight answers, each naming the ownership the type expresses — not the
ownership the author intended. Two of them leak: (b) unless allocate's result is deleted
somewhere, and (e) the moment anybody copies an E. One of them, (g), is a non-owning raw pointer
that is fine because parent_ outlives the child — say how you know that.
Stuck? Section 3 and section 10 of Concepts.
W2 — Write an RAII wrapper for a non-memory resource¶
Task. Write a wrapper for a resource with an explicit release step — a lock, a connection, or
the device shim from examples/module-11/01-raii-across-exit-paths. It must release exactly once on
the normal path, on an early return, and when an exception is thrown. It must not be copyable.
Success looks like. The class, plus three runs of the same scenario with counters showing the release count returning to zero each time. One of the three must end in a caught exception.
Stuck? Section 2 of Concepts, and Part 2 of the example program.
W3 — Convert three raw owning pointers¶
Task. Find or write three raw owning pointers — a member with a destructor that deletes it, a local that is deleted before a return, and a container of raw pointers that are deleted in a loop. Convert each to a smart pointer, and fix every compilation error the change produces.
Success looks like. The program builds, behaves identically, and you can explain each error:
which call needed std::move, which parameter had to change from T* to T&, and which container
became a container of owning pointers. Keep a list of the changes; it is the content of Lab 11's
first requirement.
Stuck? Section 10 of Concepts — the conversion table.
W4 — Balance the books¶
Task. For the program below, say how many owners each object has at the point marked, how many destructor lines print at the end, and whether anything leaks.
struct Item { ~Item() { std::cout << "item gone\n"; } };
int main() {
auto first = std::make_shared<Item>();
auto second = first; // (1) how many owners?
{
auto third = second; // (2) how many owners?
std::weak_ptr<Item> watcher = first; // (3) does this add an owner?
} // (4) does anything print here?
first.reset(); // (5) does anything print here?
std::cout << "end of main\n";
} // (6) and here?
Success looks like. Six answers, with item gone printed exactly once — after the last owner
dies, not at (4) and not at (5). Note that (3) does not add an owner, which is the entire purpose of
weak_ptr.
Stuck? Part A of examples/module-11/04-shared-and-weak/main.cpp, which prints use_count() at
each of these moments.
Core [L2 Apply] · [L3 Modify] · [L4 Analyze]¶
C1 — Fix the container that leaks on one derived type [L3 Modify]¶
Task. A std::vector<Base*> holds three derived objects. All three are deleted in a loop, and
only one of them leaks its own internal buffer. Fix the class — not the container.
struct Base {
virtual void use() const = 0;
~Base() = default; // ← find it
};
struct DerivedWithBuffer : Base {
DerivedWithBuffer() : buffer_(new char[1024]) {}
void use() const override {}
private:
char* buffer_; // ← and here
};
Success looks like. The leak gone, and a named cause in two parts: the base has no virtual
destructor, so deleting a DerivedWithBuffer through Base* never runs its destructor and never
frees the buffer; and the derived class owns a raw resource with no Rule of Three. The fix touches
the base's destructor and the derived's ownership, in that order, and the container's code does not
change at all.
Stuck? Module 7 for the virtual destructor, and Module 10 for the ownership.
C2 — Break the cycle and prove the destructors now run [L3 Modify]¶
Task. A parent/child graph hangs on to its objects forever. Find the cycle, break it with
weak_ptr, and prove the destructors now run — with output before and after.
struct Child {
std::shared_ptr<Parent> parent; // owning link back up the graph
~Child() { std::cout << "child destroyed\n"; }
};
Success looks like. Before: no destructor lines at all. After: the lines appear, in an order you
can explain, and parent.lock() still works while the parent is alive. Explain why the reverse
order of destruction is the only order possible once the back-link stops owning.
Stuck? Part C of examples/module-11/04-shared-and-weak/main.cpp; section 7 of
Concepts.
C3 — Two control blocks for one object [L4 Analyze]¶
Task. This program deletes one object twice. Explain the crash, then fix it two different ways — one that changes the constructor calls, and one that changes the class.
struct Surface {
std::shared_ptr<Surface> self() { return std::shared_ptr<Surface>(this); } // ← and here
};
Success looks like. A correct diagnosis (two control blocks, two counts, two deletes — and
use_count() printing 1 twice), two fixes that build and run, and a stated preference with a
reason. The class-level fix is std::enable_shared_from_this<Surface> and shared_from_this(); the
call-site fix is to copy an existing shared_ptr rather than construct a new one from the raw
pointer.
Stuck? Part D of examples/module-11/04-shared-and-weak/main.cpp; section 8 of
Concepts.
C4 — The parameter that promises too much [L4 Analyze]¶
Task. A function takes a std::shared_ptr<T> by value and never stores it. Say what that costs,
what it promises, and what the signature should be. Then find and change one such function in your
own Lab 10 code, or in this module's examples.
Success looks like. An answer naming the atomic increment and decrement per call, the false
promise that the callee is now an owner, and both correct alternatives (const T& when the argument
must exist, T* when it may not). Plus the changed signature in real code, with a one-line note of
what callers now do differently.
Stuck? Section 5 of Concepts; the three signatures in Part B of
examples/module-11/03-unique-ownership/main.cpp.
Challenge [L5 Design] · [L7 Refactor] · [L8 Evaluate]¶
X1 — Refactor Lab 10's catalogue to express all ownership in types [L7 Refactor]¶
Task. Take the Rule-of-Zero catalogue from Lab 10 and replace every raw pointer with an owning
type: unique_ptr for the catalogue's items, an RAII guard for the report file it opens, and a
back-reference from each loan to its item that does not keep the item alive. The observable
behaviour must not change.
Success looks like.
- Identical output from the same scripted sequence, and no hand-written special member functions anywhere in the ownership path.
- Every label on Lab 9's ownership diagram now appears in a declaration — that mapping is the deliverable.
- Counters proving acquisition and release balance on the normal path and on a path where an exception is thrown mid-operation.
- One written paragraph: the place where you rejected
shared_ptr, and why.
How you will be judged. On the proof, and on the mapping. A refactor that changes behaviour and says so honestly scores higher than one that changed behaviour and did not notice.
X2 — Fix a leak without adding a try/catch [L7 Refactor]¶
Task. A working program leaks one resource on an error path. Fix it with the smallest change
that makes release automatic. A catch block that cleans up is not an acceptable answer, and
neither is an extra close() before each return.
Success looks like. The leak gone, the diff small — a wrapper object or a smart pointer in place
of the raw handle — and a before/after counter run showing the release count returning to zero on the
path that used to leak. Explain why the catch approach was rejected: it has to be repeated at every
exit point, including the ones added later.
How you will be judged. On the size of the diff and on whether the fix survives a new early
return added to the function. Add one and re-run: an RAII fix is unaffected, a catch fix is not.
X3 — Judge two ownership designs [L8 Evaluate]¶
Task. Design the same subsystem twice: once with shared_ptr throughout, and once with
unique_ptr plus references for borrowing. Then evaluate both on correctness, cost, clarity and
maintenance, and say which you would ship.
Success looks like.
- Both designs build and run, with the same behaviour.
- Measurements where they are measurable:
sizeofthe owning handles, count the atomic operations (instrument the copy and destruction of the pointers, or count them throughuse_count()calls), and show the reference-count traffic difference. - A real cost of the design you chose. For the
shared_ptrdesign that is usually the cycle risk and the atomic traffic; for theunique_ptrdesign it is usually the extra parameter plumbing and the fact that a caller cannot keep an object alive by holding a copy. - A decision with reasons, and one sentence on what would change your mind.
How you will be judged. On the named cost of your own choice. "One is simply better" is the one answer that scores nothing, because practitioners hold both positions and both have evidence.
Stretch (optional)¶
- S1 — "What changes if…" the cycle is three nodes instead of two? Does breaking one link still fix it, and does it matter which one? Prove it with destructor output.
- S2 — "What changes if…" the
weak_ptris replaced by a raw pointer in the same graph? Which bugs become possible, and which become impossible? - S3 — "What changes if…" the deleter is a lambda that captures state? Where does the capture
live, and how does the
unique_ptr's size change?
Solutions¶
There are no separate solution files for these exercises. Each one extends a program
that already exists, and the exercise says which — the references named most often are
examples/module-11/01-raii-across-exit-paths/, 03-unique-ownership/ and
04-shared-and-weak/.
Attempt first: reading a reference before attempting destroys its value, because you will recognise it later and mistake that for understanding.
Next: Lab 11 — every resource owned by exactly one thing, with proof on both the normal and the throwing path.