Module 3 — Lab 3: instrument the system, and predict its trace¶
Levels: [L2 Apply → L4 Analyze] · Extends: Lab 2 — the same program, with types · Time: 3–4 hours
Where the earlier lab's brief lives
Lab 2's brief is on Module 2's page. Each lab's brief is published with the module that teaches its technique; the index of all sixteen is Labs.
The scenario¶
The library system from Lab 2 works: Book, Member and Catalogue, split across a
header and a source file, with the limit of three enforced by Member.
A colleague has asked a question that sounds harmless and is not:
"How many
Bookobjects does one 'lend an item' actually create? And when do they get destroyed?"
Nobody can answer it by reading the code, because the answer depends on copies, temporaries and scope endings — none of which appear in the source as objects. This lab makes the program answer the question for itself, and it makes you commit to an answer first.
★ The prediction is the deliverable. A trace without a prediction is output; a trace with a prediction is evidence about your model of the language. The lab is graded on the prediction, the comparison, and the explanation of the differences — in that order.
Part A — the reduced scenario (do this first)¶
The reference solution contains a small standalone program that isolates the question. Do Part A before Part B: it is where the mechanism lives, and Part B is where it gets interesting.
examples/labs/lab-03/solution/main.cpp defines a Tracked class that prints every
construction, copy and destruction, with a static live counter, and a main with five
scripted steps:
- two objects in the outer scope;
- one passed by value to a function;
- one returned by value from a function;
- one created in a nested scope;
- the end of
main.
Do not run it yet. Write the expected trace, line by line, including the live counter
— into a file called prediction.md in your own work folder. Then run the program and
append a table of every difference, with the mechanism named for each.
Two of the five steps are where nearly everybody is wrong, and they are wrong in opposite directions: one step adds two lines you may not expect, and one adds nothing when you expect it to. Finding out which is which is the lab.
Build and run:
Part B — instrument the library system¶
Now do it to the program that matters. Take your Lab 2 solution and add the same
instrumentation to Book, Member and Catalogue:
- a print in every constructor (and in the copy constructor, if the compiler generated one, you must write it yourself so that it can print);
- a print in every destructor;
- a
staticlive counter per class.
Then run this session — the same commands Lab 2 used — and answer the colleague's question:
./library <<'EOF'
A 1001 C++ Primer
A 1002 Clean Code
M Alice
L 1001 Alice
L 1002 Alice
R 1001
P
EOF
Deliverable: the instrumented trace, and four answers in writing:
- How many
Bookobjects were constructed in total, and how many copies were made? Which line of the session caused each copy? - Did the live counter for each class return to
0at exit? If not, which object was never destroyed, and where was it created? - Which of your
Catalogueoperations would be affected first if aBookbecame expensive to copy? Name the operation and the line that copies. Catalogue::addandCatalogue::lendboth take arguments. Change one of them so that it no longer copies, and show the trace shrinking. Then say what the change cost — what can the function no longer do?
Requirements¶
- A written prediction, before any run, for Part A — at least twelve lines, including counter values. Appended, not rewritten, after the run.
- A difference table for Part A: every line that differed, the mechanism, and the language rule rather than the code change.
- Instrumentation on all three of your Lab 2 types, using the four-line pattern from
Debugging:
distinct prefix per event, object name, live counter,
'\n'notstd::endl. - A live counter per class, and a statement at the end of the run about whether it returned to zero — with the object named if it did not.
- One operation changed to avoid a copy, with the before-and-after trace lines and a sentence on what the change cost.
- The instrumentation removed again, in a separate copy of the program, building with no warnings. The instrumented version is a diagnostic; the clean version is what you keep.
- No warnings from
g++ -std=c++17 -Wall -Wextrain either version. - Your Lab 2 program must still pass its own checks after the instrumentation is removed — same input, same output as before this lab.
Requirement 8 is the one that catches people out
Instrumenting changes output, and un-instrumenting has to restore it exactly. Keep the
Lab 2 trace in a file (a scripted session's output) so you can diff before and after.
This is the first lab where "I changed it and changed it back" is a testable claim, and
it is good practice for Module 16's test suite.
What to deliver¶
prediction.md— the prediction, the trace, and the difference table.- The instrumented program, in whatever form you like.
- The clean program, unchanged in behaviour from Lab 2.
- The four answers from Part B, in writing, with line numbers or code quoted.
Self-assessment checklist¶
The prediction is real
- [ ] It was written before the first run, and it is in the repository/hand-in, not only in your head
- [ ] It includes the live-counter values, not just the text lines
- [ ] Every difference in the table names a mechanism (by-value parameter, temporary, elision, reverse order, delegation) and not just "unexpected"
- [ ] At least one prediction was wrong, and you can say why your model was wrong
The instrumentation is right
- [ ] All three types print on construction, copy and destruction
- [ ] The copy constructor is written by hand where the compiler would have generated it — otherwise copies are invisible, which defeats the exercise
- [ ] Each class has its own live counter, and it is
static - [ ] The output is readable: prefixes, object names, no flushing per line
The Part B answers are specific
- [ ] Every copy is traced to the line of the session that caused it
- [ ] The counter question is answered with an object name, not "looks fine"
- [ ] The copy-avoidance change is shown with before-and-after trace lines
- [ ] The cost of that change is named — what the function can no longer do
The system still works
- [ ] The clean version builds with no warnings
- [ ] Its output on the Lab 2 session is byte-identical to Lab 2's output
- [ ] No leftover debug printing anywhere
Common ways this lab goes wrong¶
| Symptom | Cause | Fix |
|---|---|---|
| The trace shows no copies at all | The compiler's copy constructor was never instrumented — it exists, it just prints nothing | Write the copy constructor by hand so it can announce itself. This is the single most common failure in this lab, and it makes the whole trace useless |
| The prediction was written after the run | Nobody is watching, and the exercise is worth nothing | Delete it and write an honest one for a different small program, then do the real one. This lab is only as valuable as the prediction's honesty |
| Counter ends at a non-zero value | An object was created with new, or a static counter was declared in the header without a definition |
Check for new (there should be none in Lab 2), and check the counter's definition |
| The instrumented build produces output that breaks a scripted session | Prints were added to describe() as well as to the special members |
Instrument only the four special members |
| The clean version no longer matches Lab 2's output | A print was missed when removing them, or a describe() was changed "while I was there" |
diff the two outputs. This requirement is deliberately mechanical |
undefined reference to Tracked::live_ |
A static data member is declared in the class and defined nowhere |
Define it once outside the class: int Tracked::live_ = 0; (Module 5 covers why) |
Reference solution¶
examples/labs/lab-03/solution/ — main.cpp (the instrumented scenario) and
prediction.md (the reference prediction, the actual trace, and the difference table).
Two things to look at after your own attempt:
- The reference prediction is wrong twice, and one of the two errors is a
"temporary" that the language never creates — because C++17 constructs a returned
prvalue directly in the caller. Being wrong in a documented way is the honest version
of this lab, and it is why
prediction.mdis published with the solution rather than hidden. - The claim about
-std=c++17in the solution's header comment is testable: build with-std=c++14and watch the trace gain lines. That experiment is the reason the course fixes the standard in every command.
The reference scenario is smaller than your Part B program on purpose. A prediction you can write in one sitting is a prediction you can be wrong about usefully; a fifty-line trace is neither.
Next: Challenge problems — immutable records, owning classes, and the refactor that makes the compiler's functions correct again. · All modules