Skip to content

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:

g++ -std=c++17 -Wall -Wextra -Werror main.cpp -o exercise && ./exercise

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: sizeof the owning handles, count the atomic operations (instrument the copy and destruction of the pointers, or count them through use_count() calls), and show the reference-count traffic difference.
  • A real cost of the design you chose. For the shared_ptr design that is usually the cycle risk and the atomic traffic; for the unique_ptr design 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_ptr is 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.