Module 10 — Exercises¶
Every exercise states its cognitive level, its task, and the observable result that counts as success. If you cannot tell whether you succeeded without reading a solution, the exercise is not finished — say so, and sharpen it.
Work in a scratch directory. Compile with the course's flags, and treat a warning as a failure:
Warm-up [L1 Understand]¶
W1 — Read the class, name the operations¶
Task. For each class below, write a table of the five special member functions: which one is user-declared, which the compiler generates, and which is deleted.
// (a)
struct A { std::string name; std::vector<int> marks; };
// (b)
struct B { ~B(); std::string name; };
// (c)
struct C {
C(const C&) = delete;
C& operator=(const C&) = delete;
};
// (d)
struct D { D(D&&) noexcept = default; std::string name; };
// (e)
struct E {
~E() = default;
E(const E&) = default;
E& operator=(const E&) = default;
E(E&&) noexcept = default;
E& operator=(E&&) noexcept = default;
};
// (f)
struct F { std::unique_ptr<int> owned; };
Success looks like. Six tables, each consistent with section 3 of
Concepts, and one sentence for the two that are copied-moveable at all: what
happens if you copy A, and what happens if you copy D?
Stuck? Re-read section 3. The row you need is "You declared → Compiler generates".
W2 — Say what std::move does without using the word "move"¶
Task. In one sentence, define std::move(x) without using the word move and without using
the word cast twice. Then answer: given only std::move, what decides whether a copy or a
move actually happens?
Success looks like. "It converts x into an rvalue, so that an overload taking T&& can be
selected." And: the function that gets selected decides — std::move itself never touches the
object.
Stuck? Section 7 of Concepts, and Part A of
examples/module-10/04-move-semantics/main.cpp, which proves the claim with static_assert.
W3 — Prediction without running¶
Task. Predict the output of this program exactly, then run it and compare. Predict the
number of dtor lines before you look at the code more than once.
#include <iostream>
struct Own {
int* p;
explicit Own(int v) : p(new int(v)) { std::cout << "ctor\n"; }
~Own() { std::cout << "dtor\n"; delete p; }
};
int main() {
Own a(1);
Own b = a;
*b.p = 99;
std::cout << *a.p << '\n';
}
Success looks like. Your prediction includes the print of 99 (the two objects share one
int), and you can say where the bug is even though the program's behaviour after the second
dtor is undefined. On this machine the program may survive; that is not evidence of
correctness.
Stuck? Section 1 of Concepts.
W4 — Which of these needs a destructor?¶
Task. For each class, say whether a destructor is required, forbidden, or optional — and what the class owns.
class Timetable { Header header_; SessionList sessions_; Course* owner_ = nullptr; };
class Catalogue { std::vector<Item> items_; };
class Connection { int fd_; };
class Observer { Subject& subject_; };
class Registry { Registry* parent_ = nullptr; }; // parent outlives the child
Success looks like. Four answers justified by ownership, with "required" reserved for
Connection (a non-memory resource that must be released) and "forbidden" for Timetable
(freeing a borrowed pointer would be worse than leaking it).
Stuck? Module 9's value, pointer or reference example.
Core [L2 Apply] · [L3 Modify] · [L4 Analyze]¶
C1 — Fix a class that both leaks and double-frees [L3 Modify]¶
Task. This class leaks on assignment and double-frees on destruction. Fix it, and say which fix addresses which symptom.
class Bag {
public:
explicit Bag(std::size_t n) : size_(n), data_(new int[n]{}) {}
~Bag() { delete[] data_; }
Bag& operator=(const Bag& other) {
size_ = other.size_;
data_ = new int[other.size_]; // the leak: the old block is still ours
std::copy(other.data_, other.data_ + other.size_, data_);
return *this;
}
private:
std::size_t size_;
int* data_;
};
Success looks like. A class with a copy constructor and a correct assignment operator, and
counters proving allocations == frees and blocks alive == 0; plus two sentences pairing
symptom to cause. Note that the shallow copy is a third defect — the missing copy constructor —
and that your fix must address all three.
Stuck? examples/module-10/02-rule-of-three/main.cpp is the correct version of this class,
including the guard.
C2 — Add move operations and prove they are used [L3 Modify]¶
Task. Add a move constructor and move assignment operator to your fixed Bag, both
noexcept. Then write a test that shows, with counters, a printed scene where a copy was
happening and now is a move.
Success looks like. Before/after output for the same scripted sequence: the number of copies falls, the number of moves rises, and the allocations do not. State the before and after numbers in a comment.
Stuck? Part D of examples/module-10/04-move-semantics/main.cpp for the counters, section 7
of Concepts for the syntax.
C3 — The vector that copies on every grow [L4 Analyze]¶
Task. This type is stored in a std::vector and the vector copies it on every reallocation.
Reproduce that, explain the cause, then fix it with the smallest change.
class Reading {
public:
Reading(int id, std::string sensor) : id_(id), sensor_(std::move(sensor)) {}
Reading(const Reading& other) : id_(other.id_), sensor_(other.sensor_) {}
Reading(Reading&& other) : id_(other.id_), sensor_(std::move(other.sensor_)) {} // ← find it
private:
int id_ = 0;
std::string sensor_;
};
Success looks like. The noexcept counts before and after: seven copies and zero moves
before, zero copies and seven moves after — the same two lines Example 5 prints. Then a one-line
static_assert(std::is_nothrow_move_constructible<Reading>::value, "…") that would have caught
it at compile time.
Stuck? examples/module-10/05-noexcept-moves/main.cpp, and section 8 of
Concepts.
C4 — What did that destructor remove? [L4 Analyze]¶
Task. A class has a destructor, and nothing else. List everything that changed in its interface as a result, and demonstrate one consequence.
cpp
class Session {
public:
~Session() { /* closes a file */ }
private:
std::string path_;
};Success looks like. A correct list — no move constructor and no move assignment are
generated, while the copy operations still are (their implicit generation is deprecated, but
they exist), so the class is copyable and never moved — and a demonstration that puts a
Session into a std::vector and shows copies where moves were expected.
Stuck? Section 3's table, and the note: in Mistake 2 of
Misconceptions and mistakes.
Challenge [L5 Design] · [L7 Refactor] · [L8 Evaluate]¶
X1 — Rule of Five to Rule of Zero, with proof [L7 Refactor]¶
Task. Take the owning class you fixed in C1 (with its counters still in place) and refactor it to the Rule of Zero using standard containers. Delete the destructor, the copy constructor, the copy assignment operator and both move operations. The observable behaviour must not change.
Success looks like.
- Before/after source, with the special members visible in the "before" and absent in the "after".
- The same scripted sequence of operations producing identical output, except for the counts that are now measuring different internals.
- A short written account: what disappeared, what stayed correct, and which behaviour changed (if any) and why that is acceptable.
How you will be judged. On the proof, not the refactor. Identical output from a scripted sequence is the evidence; a claim that "it looks equivalent" is not. The reference solution keeps all three stages — broken, Rule of Five, Rule of Zero — for exactly this comparison, including the stage where its author got the copy-and-swap ordering wrong.
X2 — Design a movable-only handle [L6 Build]¶
Task. Design and build a small type that owns an exclusive system resource — model it as a file handle, a socket, or a device in a device pool — which cannot be copied, can be moved, and releases exactly once.
Success looks like.
std::is_copy_constructible<Handle>::valueisfalseandstd::is_move_constructibleistrue, both asserted at compile time.- A counter or a sanitizer proving the release happens exactly once, on both the normal and the exceptional exit path.
- One paragraph on what callers must do differently now that copying is impossible, and why that is a feature of the design rather than a limitation.
How you will be judged. On whether the type's interface communicates ownership — a caller reading the declaration should be able to say "I can move it, I cannot copy it" without reading the implementation.
X3 — Judge two implementations [L8 Evaluate]¶
Task. Write the same owning class twice: once as a hand-written Rule of Five, once as a Rule of Zero class built from standard members. Then evaluate them on correctness, readability, performance and maintenance, and say which you would ship.
Success looks like.
- Two working versions and the same scripted sequence run against both.
- A judgement that names a real cost of the version you chose — for example, the Rule of Zero version cannot expose the raw buffer the way the hand-written one can, or its allocation pattern is different in a way that showed up in your counters.
- No claim that either is simply better. If your evaluation says "the Rule of Zero is better in every way", you have not looked at your own counters closely enough.
How you will be judged. This is the first level-8 activity in the course, and it is deliberately small: judging two files is the simplest honest form of design evaluation. The reasoning is assessed as much as the code.
Stretch (optional)¶
- S1 — "What changes if…" you add a
mutablemember counter to a class and copy it? Does the counter travel with the copy, or is it shared? Write down the answer, then check it. - S2 — "What changes if…" you declare
Buffer(Buffer&&)withoutnoexceptand put it in astd::maprather than astd::vector? Find the answer by measurement, not by reading. - S3 — "What changes if…" a class has
const std::string name_;as a member? Predict the effect on moves, then confirm it withstd::is_move_assignable<const_name_class>::value.
Solutions¶
There are no separate solution files for these exercises. Each one extends a program
that already exists, and the exercise says which: the reference for the hand-written
operations is examples/module-10/02-rule-of-three/, for the moved-from state
04-move-semantics/, and for the promise a move makes 05-noexcept-moves/.
Attempt first: reading a reference before attempting destroys its value, because you will recognise it later and mistake that for understanding.
Next: Lab 10 — the catalogue, reproduced broken, fixed, and then refactored until it cannot break.