Module 10 — Challenge Problems¶
Open problems. More than one defensible answer exists, and the reasoning is assessed as much as the code.
Each one asks for a decision, a piece of evidence, and a written account of the trade-off you accepted. Write the account before you compare with anything — including the reference notes at the end of this page.
Challenge 1 — Refactor a bespoke fixed-size buffer [L7 Refactor]¶
The problem. This class hand-manages a fixed-capacity allocation, and it has behaviour that must survive the refactor:
class FixedList {
public:
explicit FixedList(std::size_t capacity) : capacity_(capacity), size_(0), data_(new int[capacity]) {}
~FixedList() { delete[] data_; }
bool push(int value) { // returns false when full — does NOT grow
if (size_ == capacity_) { return false; }
data_[size_++] = value;
return true;
}
void clear() { size_ = 0; }
std::size_t size() const { return size_; }
std::size_t capacity() const { return capacity_; }
int at(std::size_t index) const { return data_[index]; }
private:
std::size_t capacity_;
std::size_t size_;
int* data_;
};
Refactor it to use a standard container, keeping every observable behaviour — including what happens when it is full, and including the fact that it does not grow.
What a good answer contains. Identical output from an identical sequence (fill to capacity,
push one more, clear, refill), a class with no special member functions, and a written account of
the one behaviour that changed. There is one: a std::vector with reserve(capacity) will not
grow while you use push_back up to capacity, but push_back beyond it will grow silently, so
the capacity check has to stay in your push. Say that in words, and say why moving the check
into the container is not possible.
What would make it weak. A version that changes the "returns false when full" behaviour into
exceptions without saying so, or a version that quietly makes at() unchecked when the original
was unchecked too — but does not state which one it kept.
Deciding it for yourself. Ask which of the class's public behaviours a caller can observe, and make the list explicit before you change anything. A refactor is only provably behaviour-preserving relative to a written list.
Challenge 2 — A movable-only handle to an exclusive resource, with proof [L6 Build]¶
The problem. Design a type that owns an exclusive system resource — a file handle, a socket, or an entry in a device pool — with these properties:
- It cannot be copied, in either direction.
- It can be moved, leaving the source in a valid but clearly empty state.
- The resource is released exactly once, whichever way the scope exits.
- Transferring ownership is visible in the type's signature, not in a comment.
Then prove all four properties.
What a good answer contains.
- The declaration itself, with deleted copies and
noexceptmoves, and astatic_assertor astd::is_copy_constructible/std::is_move_constructiblecheck for properties 1 and 2. - Evidence for property 3: counters showing
acquires == releases == 1, or a sanitizer report showing no leak on both the normal and the exceptional path (returnand a caught exception in the same scope). - Property 4 demonstrated: a function
Handoff take()returning by value, and a call site that compiles; then a call site that tries to copy it and does not, with the message quoted. - A paragraph on what callers must now do differently — and why that is a feature rather than an
inconvenience. This is where most answers are thin: the honest answer is that a non-copyable type
forces ownership questions to be answered at compile time, at the price of more
std::movenoise in the source.
What would make it weak. A type that is copyable "because it was easier", or one whose moves
are not noexcept (property 3 still holds for hand-written code, but the type becomes unsuitable
for standard containers — say so if you made that choice deliberately).
Deciding it for yourself. Whether a released-and-reused handle type needs a release() that
returns the raw resource. If you add one, you have reintroduced manual ownership; decide whether
that is a deliberate escape hatch for interoperability with C APIs, and document it as one.
Challenge 3 — Judge two implementations of the same class [L8 Evaluate]¶
The problem. You have two working implementations of the same owning class:
A. Hand-written Rule of Five: a raw pointer member, five special members, all correct, all counted.
B. Rule of Zero: standard member types, no special members at all.
C. (= your own third option, if you can think of one)
Evaluate them on correctness, readability, performance and maintenance, say which you would ship for each of three settings — a teaching codebase, a hot inner loop, a long-lived library — and justify each.
What a good answer contains.
- The same scripted sequence run against both (or all three) implementations, with the counters reported side by side.
- A named, real cost of whichever implementation you prefer. For the Rule of Zero that is usually one of: less control over allocator behaviour, a different allocation pattern that your counters actually showed, or an interface that cannot expose the raw buffer to a C API. For the Rule of Five it is usually: five functions to keep correct forever, and a bug class that reappears every time a member is added.
- A distinction between correctness (unquantified: both are correct) and the probability of staying correct, which is where the two differ and where your real answer lies.
What would make it weak. "The Rule of Zero is better in every way." If your measurements say that, you did not measure the thing that differs — ownership policy, not ownership correctness. Similarly weak: declaring one implementation "more educational" without saying what a reader of it learns that the other hides.
Deciding it for yourself. Which of the two you would want to debug at 2 a.m. six months from now, and why. Then check whether that reason is the same one you gave for the other settings. If it is not, you have found a genuine context dependence, which is the answer this challenge is looking for.
Evaluating your own answer¶
Write these three lines before reading any suggested approach:
Then, for Challenge 3 specifically, add a fourth line — and expect it to be uncomfortable:
Level: [L7 Refactor] · [L6 Build] · [L8 Evaluate] — the exit level of this module is 7, and Challenge 3 goes one beyond it.
Next: Self-assessment — nine questions, three of which are predictions.