Practice Lab 19 — Dynamic collection: Rule of Three to Rule of Five¶
Difficulty: Advanced · Estimated time: 6 hours · Module 10: Copying, Assignment and Resource Management
You will write the double free, watch it happen, and then take the rule of three and the rule of five all the way to the rule of zero. The counters in this lab are the evidence.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 6 hours |
| Module | Module 10 — Copying, Assignment and Resource Management |
| Concepts practised | Copy constructor, copy assignment, destructor, shallow versus deep copy, self-assignment, copy-and-swap, move constructor and move assignment, noexcept, rule of three/five/zero |
| Prerequisite | Practice Lab 18 |
| Class design | giving you the class; you own its copy and move behaviour |
Scenario¶
A measurement archive holds a growing number of samples in memory, and the archive is copied by other parts of the program: one copy per report, one for a backup, one for a comparison. Copies are the requirement, not a mistake.
The archive's first version used a std::vector internally and needed no copy code at all. The second
version, rewritten for reasons nobody remembers, holds a double* and a size. It now crashes about once a
week, and the crash always happens after two archives are assigned to each other.
Your job is to make the raw-pointer version correct, and then to show — with counter evidence — why the first version should have been kept.
Problem statement¶
Implement a SampleArchive that owns a dynamically allocated array of double samples.
- Reproduce the failure: write the version with no copy constructor, no copy assignment and a destructor that deletes the array. Copy an archive, destroy one, and show what happens.
- Apply the rule of three: copy constructor, copy assignment with self-assignment safety, destructor.
- Add the rule of five: move constructor and move assignment, both
noexcept, leaving the moved-from archive in a documented valid-but-empty state. - Show the difference: instrument every operation with a counter — copies made, moves made, allocations made — and print the counters for a scripted sequence.
- Reach the rule of zero: a second type that holds a
std::vector<double>instead, with no copy or move code at all, run through the same sequence, with the same counters. - Measure: for a sequence of 100 000 appends, report the number of allocations in each version, and the
effect of a move constructor that is not
noexcept.
In copying-notes.md:
- The failure from step 1, verbatim, with the output.
- The counter table for the three versions, side by side.
- One sentence per special member function saying what it must do and why.
- The sentence you would give a colleague about when the rule of five is the wrong answer.
Learning objectives¶
By the end of this lab you will be able to:
- Reproduce the double free and explain it as "two destructors for one allocation".
- Write the five special member functions correctly, including self-assignment safety and
noexceptmoves. - Use copy-and-swap, and explain what guarantee it gives when an allocation fails.
- Show, with counters, the difference between copying and moving in a real sequence.
- Explain the rule of zero and demonstrate why it is usually the right answer.
Functional requirements¶
SampleArchiveowns its array; the destructor releases it exactly once.- The copy constructor allocates its own array; a copy is independent — prove it by changing the copy and printing both.
- Copy assignment is self-assignment safe — prove it by
archive = archive;. - Copy assignment either gives the strong guarantee or is documented as basic; say which, and demonstrate.
- The move constructor leaves the source empty and valid; the moved-to archive owns the original array.
- Move operations are
noexcept, and you must show what happens to a vector's reallocation count when they are not. Record the difference. - The moved-from archive can be destroyed, assigned to, and queried without crashing.
- The rule-of-zero version has no user-declared copy, move or destructor — prove it by showing the class body.
- Counters: copies, moves, and allocations, printed after each scripted phase.
- All samples in a copy equal the original's, and the sizes match, after every operation.
Constraints¶
- Two headers and their sources, plus
main.cpp; documented build command. - Raw
double*in the first version, deliberately, withnew[]/delete[]. It may not be replaced byunique_ptruntil Module 11 — but you must state what that replacement would remove. - No
std::vectorin the first version.std::vectoris the rule-of-zero version. - No warnings, and no
-Werrorsuppressions: if the compiler warns about something real, fix the design. - Every special member function narrates, with its name — that is how the counters are collected.
Starter task¶
In copying-notes.md, before coding:
- Write the six declarations of the rule of five, with their signatures and
noexceptwhere it belongs. - Predict, for
a = b;where both own arrays: how many allocations, how many copies, how many moves. - Predict what happens in the un-instrumented version 1 when you copy an archive and destroy the copy.
- Predict the reallocation count for 100 000 appends with and without a
noexceptmove constructor, and say which one you expect to be larger. Then measure.
Implementation tasks¶
- Implement version 1 (no special members), copy, destroy, and record the failure.
- Implement version 2 with the rule of three, including self-assignment and the guarantee statement.
- Extend it to the rule of five with
noexceptmoves. - Implement version 3 with
std::vectorand no special members. - Write the scripted sequence and print the counters for each version.
- Measure the append sequence in both versions and with a non-
noexceptmove, and record the numbers.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Version 1: copy then destroy both | The recorded failure — do not "fix" it before recording |
| 2 | Copy an archive, change a sample in the copy | The original is unchanged |
| 3 | archive = archive; |
No corruption; counters show the documented behaviour |
| 4 | Assign a smaller archive to a larger one | The target owns exactly the source's data |
| 5 | Move into a new archive | No allocation; source becomes empty and valid |
| 6 | Destroy a moved-from archive | No crash, no double free |
| 7 | Use a moved-from archive (size, append) | Works, with the documented empty semantics |
| 8 | 100 000 appends with noexcept moves |
A reallocation count you record |
| 9 | The same with a move that is not noexcept |
A larger count, or the reason it is not, recorded |
| 10 | Rule-of-zero version through the same script | Identical results, no user-declared special members |
| 11 | Counters table | Copies, moves and allocations, side by side |
| 12 | Assign from an empty archive | Target becomes empty; no allocation if avoidable |
Boundary cases to run deliberately: a zero-sample archive; one sample; copying an archive into itself through a reference; moving an empty archive.
Expected behaviour¶
version 1 (no special members):
copied archive, destroying the copy...
[dtor] freed 0x...
[dtor] freed 0x... <- the same address twice: the failure this lab exists for
version 2 (rule of three):
[copy ctor] allocated 5 samples
[copy assign] freed old, allocated 5 samples
archive = archive; -> self-assignment handled, 0 extra allocations
version 3 (rule of zero):
[rule-of-zero type] no user-declared special members
counters: copies 4, moves 6, allocations 5 (version 2)
counters: copies 4, moves 6, allocations 0 (version 3, reusing capacity)
100000 appends: reallocation count 17 (noexcept moves) vs 100000 (copying moves)
Extension challenge¶
Add a reserve operation to the raw-pointer version, so the archive can grow without reallocating on
every append.
Record in copying-notes.md:
- The reallocation count before and after, for the same sequence, as numbers.
- Which rule-of-five function had to change, if any, and why.
- What
reservechanges about the strong guarantee when it fails part-way. - Why
std::vectoralready does this, and what that says about writing containers by hand.
Debugging challenge¶
The crash after assignment:
SampleArchive& operator=(const SampleArchive& other) {
delete[] samples_; // free ours
size_ = other.size_;
samples_ = new double[size_];
for (int i = 0; i < size_; ++i) {
samples_[i] = other.samples_[i];
}
return *this;
}
Reproduce it — assign an archive to itself — and answer:
- What is
otherwhen the assignment isa = a, and what diddelete[]do toother.samples_? - Why does the copy-and-swap version avoid this entirely?
- Rewrite the assignment using copy-and-swap, and state the guarantee it now provides.
- Which test case in the table catches it, and did you run it before or after reading this?
Reflection questions¶
- Which of your five functions is hardest to write correctly, and what made it hard?
- A moved-from archive must be "valid", not "unchanged". What exactly does your moved-from object guarantee?
noexcepton moves changed the vector's behaviour. What does the standard library do differently, and why does it have the right to?- The rule-of-zero version needed no special members at all. Which version would you ship, and what would you have to be willing to lose?
reservereduces allocations. What does that do to the strong guarantee, and is that acceptable?- If a sample were expensive to copy (a
std::string, say), which of your functions would change, and how?
Completion checklist¶
- [ ] It builds with
-Wall -Wextra -Werrorand no warnings - [ ] The version-1 failure is reproduced and recorded before any fix
- [ ] Copy constructor allocates its own storage; independence demonstrated
- [ ] Copy assignment handles self-assignment, with the guarantee stated
- [ ] Move constructor and move assignment are
noexceptand leave a documented empty state - [ ] A moved-from archive is destroyed and used without crashing
- [ ] The rule-of-zero version has no user-declared copy, move or destructor
- [ ] The counters table covers all three versions
- [ ] The append measurement shows the
noexceptdifference, or explains its absence - [ ] All twelve test cases run, with output recorded
- [ ] The debugging challenge was explained before being fixed
- [ ]
copying-notes.mdhas one sentence per special member function
Related: Lab index · Module 10 — Copying and Resource Management · Previous: Practice Lab 18 · Next: Practice Lab 20 — Invoice copy semantics