Challenges — 6 problems

← Module home · ★ to ★★★★. Each solution ends with its design defence — write yours before comparing.


Solutions

S1.

class TxLog {
public:
    explicit TxLog(const string& path) : out(path, ios::app) {
        if (!out) throw FileError("open", path);
    }
    void append(const string& line) {
        out << line << "\n";
        out.flush();
        if (!out) throw AppError("TxLog: append failed");   // loud, never silent
    }
    ~TxLog() = default;    // ofstream closes itself — the no-fail close is the member's job
private:
    ofstream out;
};
// demonstration:
void demo() {
    TxLog log("tx.log");                 // acquired
    log.append("begin");
    doWork();                            // suppose this throws
    log.append("end");                   // skipped on the throw path
}                                        // ~TxLog → ~ofstream: file closed, no matter what

The demonstration’s point: the throw skips append("end"), unwinds, and the destructor chain still closes the file — the log is valid (flushed writes intact) and never left open. Defence sentence: “The destructor does nothing because the owner member does everything — the RAII pattern is inherited, not re-implemented.”

S2. The redesign deliverable, in the toolkit’s shape:

OWNER ARROWS (solid):  TextTool ──owns──► vector<string> lines   (the corpus)
                       TextTool ──owns──► map<string,int> tally   (the counts)
VIEW ARROWS (dashed):  printReport(const TextTool&) ┄┄views┄┄► lines, tally
                       findWord(const string&) ┄┄views┄┄► tally entries
class TextTool {
public:
    explicit TextTool(vector<string> lines) : lines(move(lines)) {}   // takes ownership by value+move
    const vector<string>& linesView() const { return lines; }          // viewers get views
    const map<string, int>& tallyView() const { return tally; }
    void rebuildTally();                    // the only tally mutator
private:
    vector<string> lines;                   // owns the text
    map<string, int> tally;                 // owns the counts
};
void printReport(const TextTool& t);        // a view: cannot outlive its argument's call

Defence: every arrow is now either a lifetime-anchored member (owner) or a call-scoped const& (view) — no third category, nothing to delete, and the construction signature (TextTool(vector<string>) by value + move) states the ownership transfer in the type.

S3.

constexpr int ROWS = 10, COLS = 10;
constexpr int CELLS = ROWS * COLS;
constexpr int cell(int r, int c) { return (r + 1) * (c + 1); }   // constexpr function is fine here
static_assert(CELLS == 100, "table shape");
static_assert(cell(0, 0) == 1 && cell(0, 9) == 10, "first row formula");

array<array<int, COLS>, ROWS> table {};
// fill with cell(r, c); print.

Changing to 12×12: CELLS recomputes, and the stale static_assert(CELLS == 100) fails compilation instantly — the assert was written against the shape, and the shape changed. That is the deliverable’s lesson: static_assert turns silent assumption into loud contract. (The constexpr function is included deliberately — constant values were the lesson; this shows the boundary honestly: simple computations are fine, machinery is not.)

S4.

class Traced {                       // the proof instrument
public:
    Traced() = default;
    Traced(const Traced&) { cout << "[copy] "; }
    Traced(Traced&&) noexcept { cout << "[move] "; }
    // ... string payload and assignment operators printing likewise ...
};

vector<string> readLines() { /* ... */ return lines; }          // A: return — automatic move/elision
vector<string> clean(vector<string> lines) {                    // B: BY VALUE — moved into
    // erase-remove whitespace-only lines; operates on its own (moved-in) copy
    return lines;                                               // automatic move out
}
int count(const vector<string>& lines) { return lines.size(); } // C: a view — no hand-off at all

// the pipeline:
auto lines = readLines();            // source: local, alive
auto cleaned = clean(move(lines));   // EXPLICIT move — we are done with lines; after: lines empty
int n = count(cleaned);              // view — no transfer, cleaned still owned here

Each comment names the source’s state at hand-off. The Traced run prints [move] at A and B and nothing at C — no copies anywhere. Defence: the pipeline’s data flows by transfer; views cost nothing; and the one explicit move sits exactly where a named variable ends its life.

S5. The policy’s shape (yours will differ; every rule needs its why):

S6. The kata is self-assessed against the Modernisation Lab’s four stages, with the addition: the complexity-of-ownership paragraph. A strong before/after reads: “The arrays-era program had four new sites and three delete paths each (normal return, two early exits), with one path provably leaky under a throw; the modernised program has zero new sites — the growth pattern became a vector, the temporary report a unique_ptr, and the only pointers remaining are the two ask-door views into find-style lookups. The tool that deleted the ownership complexity was RAII; the tool that proved it was the destructor-order trace.” If your paragraph cannot name a specific path that used to leak, pick a bigger kata program — the before must have something for the after to cure.


Every solution’s closing sentence is its design defence — compare, then run the kata on your own pre-Unit-13 code.