Skip to content

Module 10 — Lab 10: the catalogue that owns its own storage

Levels: [L3 Modify → L7 Refactor] · Extends: Lab 9 · Time: 4–6 hours (the longest lab so far)


The scenario

Your library system from Lab 9 is working, and a colleague has "optimised" it. The catalogue used to hold its items in a std::vector; it now holds a hand-managed heap array, allocated in the constructor, resized on demand, and freed in the destructor. They did it to avoid "vector's overhead", and they mention with some pride that they wrote the destructor by hand.

Last week the system began crashing at exit, most often after a large import, and never in the small test case. Nothing about the crash points at the copy.

You are going to reproduce the failure deliberately, fix it properly, and then make the whole question disappear — because the honest conclusion of this lab is that the "optimisation" was not one, and the safest implementation of a dynamically sized array in C++ is std::vector.


What you start from

Your Lab 9 program, plus this class. It compiles cleanly, passes every test you wrote, and is wrong:

class ItemStore {
public:
    explicit ItemStore(std::size_t capacity)
        : size_(0), capacity_(capacity), items_(new Item[capacity]) {}

    ~ItemStore() { delete[] items_; }

    void add(const Item& item) {
        if (size_ == capacity_) { grow(); }
        items_[size_++] = item;
    }

    std::size_t size() const { return size_; }
    const Item& at(std::size_t index) const { return items_[index]; }

private:
    void grow() {
        capacity_ *= 2;
        Item* bigger = new Item[capacity_];
        std::copy(items_, items_ + size_, bigger);
        delete[] items_;
        items_ = bigger;
    }

    std::size_t size_;
    std::size_t capacity_;
    Item* items_;
};

Read it before you type it. Two of the defects are deliberate and reproducible; a third will bite you only when you write the fix for the first two.


Requirements

  1. Reproduce the double free. Write a main (or a test function) that copies an ItemStore and then lets both objects go out of scope. Keep the evidence. (interacts with Module 9: this is your ownership model meeting the compiler's copy constructor.)

  2. Instrument before you fix. Add allocation and destruction counters to ItemStore, print them, and record the run where allocations != frees or a second free is attempted. Add an identity print in the destructor — this, and the address of the array — so that the two objects freeing one block are visible in the output.

  3. Capture tool evidence. If your toolchain has AddressSanitizer, keep the report: both stack traces, and one sentence explaining what each frame tells you. If it does not (see Debugging for the failure that indicates), say so explicitly, quote the failure, and rely on the counters — then state in writing which evidence you are missing and what it would have added.

  4. Fix it with the Rule of Five. Write the copy constructor (deep copy), the copy assignment operator (self-assignment safe), the destructor, and both move operations marked noexcept. Keep the counters.

  5. Prove the fix. A scripted sequence — construct, add, copy, assign, copy-assign into an existing store, move, destroy — whose counters end at allocations == frees, blocks alive 0, and constructions == destructions.

  6. Demonstrate self-assignment. store = store; must run cleanly, and you must show the output before and after your fix — including the version where it crashed or corrupted data.

  7. Show the moves working. A printed sequence where the number of element copies falls once the move operations exist, with the before and after numbers written down.

  8. Refactor to the Rule of Zero. Replace the hand-managed array with std::vector<Item>, delete every special member function, and run the same scripted sequence as requirement 5. The programme's output must be identical apart from the counter lines that measure internals. (The exit level of this module: refactoring with unchanged observable behaviour, proven.)

  9. Write the before/after account. Half a page, no more: what the hand-written version cost you (in defects, in lines, in evidence), what the refactor removed, and which of the counter values changed and why that is not a behaviour change.

  10. It must build with no warnings using the course's compile command:

    g++ -std=c++17 -Wall -Wextra -Werror main.cpp -o catalogue
    

What to deliver

  • broken/ — the original class with counters and the identity print, and the transcript showing the double free.
  • rule-of-five/ — the fixed class, with the counters proving the fix.
  • rule-of-zero/ — the refactored class, with the same scripted sequence and identical output.
  • The sanitizer output from the broken version, or a written statement of why there is none and what you used instead.
  • The before/after account from requirement 9.

Keeping all three stages is deliberate. The comparison between them is the thing being assessed, not the final file.


Self-assessment checklist

It builds and runs

  • [ ] Compiles with g++ -std=c++17 -Wall -Wextra -Werror with no warnings
  • [ ] All three stages produce the behaviour the requirements ask for
  • [ ] The same scripted sequence runs against all three, and you know exactly which output lines differ and why

It is correct

  • [ ] allocations == frees and blocks alive == 0 in the fixed versions
  • [ ] constructions == destructions at the end of every run
  • [ ] Self-assignment (store = store;) runs cleanly
  • [ ] Boundary cases work: empty store, exactly full, one element over capacity, a store grown several times
  • [ ] Failure cases are handled: a copy that cannot allocate, and what your class does about it

It is designed

  • [ ] I can state, in one sentence, what ItemStore owns
  • [ ] The ownership is visible in the type — Item* plus a destructor, or std::vector<Item>
  • [ ] I can say why the hand-written version was not the optimisation it claimed to be

It is verified

  • [ ] I predicted the lifecycle and allocation counts before running the fixed version, and compared afterwards — this is the second lab where that prediction is required, and here it includes the counts
  • [ ] I tested the failure path, not only the success path
  • [ ] I have the sanitizer report, or a written statement of what I used instead and what it cannot show
  • [ ] I compared my version with the reference solution and can explain every difference

It is explained

  • [ ] The before/after account names what each step bought, in specifics
  • [ ] I can say which defect each of the Rule of Five functions repairs

Common ways this lab goes wrong

Symptom Cause
The broken version does not crash The copies were elided, or you never copied it. Force the copy — pass by value, store in a container, or assign to an existing object
The counters balance but the program still crashes The counters are counting operations, not ownership. Print the block addresses too, and look for one address freed twice
The Rule of Five version leaks operator= was written to allocate before releasing, or the move constructor does not null out the source
store = store; corrupts the store The self-assignment guard is missing from copy and move assignment
The Rule of Zero version copies where the Rule of Five version moved A const member, or a member whose move is not noexcept — check std::is_nothrow_move_constructible
The output differs between stages beyond the counters A real behaviour change. Find it; it is the most interesting thing in the lab, and it belongs in requirement 9
The lab takes ten hours You are writing all three stages from scratch. Reuse: the same Item, the same scripted sequence, and the same main — only the store class changes

Reference solution

No solution file is published for this lab. It is your own Lab 9 program with its ownership made explicit, and the decisions are the work: which stage needs a destructor, which needs copy operations, which needs move operations, and which needs none of the three. A published file would turn that decision into a reading exercise.

The model code for each decision is in examples/module-10/, one program per idea, each already walked through on the concepts page where it is used:

Program The decision it models
01-shallow-copy-alias The bug this module exists for: what a copied raw pointer really shares, measured
02-rule-of-three The same owning class, fixed by hand: destructor, copy constructor, copy assignment
03-copy-and-swap The same three written so that repetition cannot hide a leak
04-move-semantics Move semantics measured one expression at a time, including the moved-from state
05-noexcept-moves Why noexcept on a move constructor is not decoration
06-rule-of-zero-and-elision The destination: the Rule of Zero, and what copy elision removes

Compare after your own version builds and runs — the value of the comparison disappears if you read a model before you have something to compare it with.


Next: Challenge problems — three open problems, including judging two implementations of the same class. · Previous: Lab 9 · Then: Lab 11 — the same ownership, expressed in types instead of comments.