Practice Lab 6 — Sensor register: constructors and temporaries¶
Difficulty: Intermediate · Estimated time: 4 hours · Module 3: Constructors, Destructors and the Object Lifecycle
The lifecycle rules stop being trivia when a value is computed from other objects. Here every temporary has a reason, and you have to name it.
At a glance¶
| Difficulty | Intermediate |
| Estimated time | 4 hours |
| Module | Module 3 — Constructors, Destructors and the Object Lifecycle |
| Concepts practised | Member initialisation lists, delegating constructors, initialisation versus assignment, temporaries, explicit, value members, const operations |
| Prerequisite | Practice Lab 5 |
| Class design | yours — a Reading and whatever holds it |
Scenario¶
A weather station reads sensors and stores the readings for a day. Each reading has a sensor identifier, a timestamp in seconds since midnight, and a value. The station also stores a calibration per sensor: an offset and a scale factor, so that a raw value becomes a calibrated one.
The station's author has a bug that only appears when a reading is created from another reading: the calibrated value is computed before the calibration is set, and the class sometimes reports numbers from the previous reading. Nobody can reproduce it by reading the code, only by watching construction order.
Problem statement¶
Design and implement:
Calibration— an offset and a scale, bothdouble, withcalibrated(raw).Reading— a sensor identifier, a timestamp, a raw value, and a calibration. Operations:calibrated_value(),describe(), andis_same_sensor(const Reading&).DayLog— up to 32 readings in a fixed array, with operations to add a reading, to find the maximum calibrated value, and to print all readings in time order.- A
Readingmay be constructed: - from a sensor identifier, timestamp, raw value and calibration;
- as a copy of another reading with a new value ("the same sensor, a moment later");
- as a default reading, which is clearly invalid and must report itself as such.
Every Reading construction narrates itself with the reason: direct, derived, or default.
The design decision you must make and defend: does the derived reading's operation belong to
Reading (a named constructor, a static factory, a member function returning a new object), or to
DayLog? Write the decision and its cost in design-notes.md.
Learning objectives¶
By the end of this lab you will be able to:
- Initialise members in an initialisation list, in the order they are declared, and explain what happens when the list's order disagrees with the declaration order.
- Use a delegating constructor and an
explicitconstructor deliberately, and say whatexplicitprevents. - Explain the difference between initialisation and assignment for a value member, in terms of what runs.
- Identify each temporary object in your program, and say why it exists.
- Avoid the bug of computing a derived value before the data it derives from is set.
Functional requirements¶
- A default
Readingis invalid: sensor identifier0, timestamp0, calibration with offset 0 and scale 1, anddescribesays so. calibrated_value()returnsraw * scale + offset.- A default
Calibrationis the identity calibration, and that fact is documented at the class. DayLog::addrefuses a 33rd reading with a message, and refuses a reading whose sensor identifier is 0.DayLog::maximum_calibrated()returns a value and a flag (or a documented sentinel) for "no readings", and never reads out of bounds.DayLog::printsorts by timestamp without reordering the stored readings — say how in your notes.- Two readings from different sensors are never reported as the same sensor.
- No operation other than
addcan change a stored reading. - Every
Readingconstruction narrates exactly once with its reason, and the counts at the end agree:direct + derived + default == reported constructions. - A
const DayLog&can be printed and searched — prove it by making the parameterconstand compiling.
Constraints¶
- Three files: a header for the two value types, a
.cppfor the log, andmain.cpp. Build with an explicit file list:g++ -std=c++17 -Wall -Wextra -Werror reading.cpp daylog.cpp main.cpp -o station - No
std::vector— a fixed array of 32, and the count kept in step with it. - Every member is initialised in an initialisation list where possible, including the
Calibrationmember ofReading. Assignment in the body is a defect in this lab, and you should be able to say why. Reading's four-argument constructor isexplicitif it could be called with a single argument in your design; justify either way in your notes.- No
new, no smart pointers, no inheritance. - No warnings.
Starter task¶
Answer in design-notes.md before writing code:
- In what order are
Reading's members initialised, and does your initialisation list agree with it? Write the declaration order out. - Which of the three constructions narrates
derived, exactly? Name the operation that creates it and say whether it returns by value. - Where does the derived reading's reason for existing live, and what would a caller have to do if it lived somewhere else?
- Name the temporaries you expect in
mainand in the derived construction, and how manyReadingnarrations each produces. Write the prediction down before running.
Implementation tasks¶
- Implement
Calibration, withcalibratedas aconstoperation. - Implement
Readingwith the three constructions, all initialised in lists, all narrating once. - Implement
DayLogwith the fixed array, the refusal rules, the maximum search and the sorted print. - Write
main: build a log of five readings, add a derived one, attempt a 33rd, attempt an invalid one, print, and print the maximum. - Compare the narration count with your prediction and record the difference.
- Move one member's initialisation from the list into the body, capture the compiler's message or the behavioural difference, and record it. Then put it back.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Default reading | describe reports it as invalid; calibrated_value is the raw value under the identity calibration |
| 2 | Reading with scale 2 and offset 1, raw 10 | calibrated_value is 21 |
| 3 | Derived reading from case 2 with raw 20 | Same sensor and calibration; value 41; one derived narration |
| 4 | Print a log with five readings added out of time order | Printed in time order, storage order unchanged |
| 5 | Maximum calibrated value | Equal to the best you can compute by hand from the printed list |
| 6 | Maximum on an empty log | The "no readings" flag/sentinel, no out-of-bounds read |
| 7 | Add a 33rd reading | Refused, with a message; count stays 32 |
| 8 | Add a reading with sensor identifier 0 | Refused |
| 9 | Print a const DayLog& |
Compiles, prints the same output |
| 10 | Narration counts at the end | direct + derived + default equals the number of readings reported |
Boundary cases to run deliberately: a log with exactly one reading; adding exactly 32 readings; two readings with the same timestamp, and what your print order does with them.
Expected behaviour¶
[reading] direct sensor 42 t=0 raw=10.0
[reading] direct sensor 42 t=60 raw=12.5
[reading] derived sensor 42 t=120 raw=20.0
[reading] default (invalid)
[daylog] refused 33rd reading: the log is full (32)
[daylog] refused: sensor identifier 0 is not valid
maximum calibrated value: 41.0 (sensor 42 at t=120)
const DayLog printed: 3 readings
constructions: direct 3, derived 1, default 1, total 5
Extension challenge¶
Add calibration expiry: a calibration applies only for the sensors it was issued to, and the log must report any reading that was calibrated with a calibration for a different sensor.
Record:
- Where the check lives, and how many places now need it.
- Whether the check belongs at construction (so an invalid reading cannot exist) or at query time (so the data is preserved). Argue it both ways in three sentences, then choose.
- The additional narration line, and whether your counts still agree.
Debugging challenge¶
Readings occasionally report the previous reading's calibrated value:
class Reading {
public:
Reading(int sensor, int timestamp, double raw, Calibration calibration)
: timestamp_(timestamp), sensor_(sensor),
calibrated_(calibrated_value()), // computed here ...
raw_(raw), calibration_(calibration) { // ... before these are set
}
private:
int sensor_ = 0;
int timestamp_ = 0;
double raw_ = 0.0;
Calibration calibration_{};
double calibrated_ = 0.0;
};
Reproduce it — the bug is order-dependent, so choose values that make it visible — and answer:
- In which order were the members actually initialised, and why that order rather than the list's?
- What value did
calibrated_hold when the constructor body ran, and where did it come from? - Which two rules of this lab does this single constructor break?
- Write the corrected constructor, and say what you would change so that the compiler could catch the mistake instead of you: a computed member, an accessor, or a rule about initialisation lists.
Reflection questions¶
Calibrationis initialised beforeraw_in the list. Why does that not matter, and when would it?- Which of your members exist only to be read, and which are computed on demand? Why did you choose
that split for
calibrated_value? expliciton a constructor prevents a silent conversion. Name one call in your program where an implicit conversion would be possible, and what it would do.DayLog::printsorted without reordering storage. What was your approach, and what is its cost compared with sorting a copy?- The 33rd reading is refused. Which is more useful to a caller: a
bool, an exception, or a message that names the limit? Answer with the caller in mind, and say what changes at Module 13. - Name every temporary object in your derived reading's construction. Which one would the compiler probably elide, and would that change your narration count?
Completion checklist¶
- [ ] It builds from the documented command with
-Wall -Wextra -Werrorand no warnings - [ ] Every member is initialised in an initialisation list
- [ ] Three constructions, each narrating exactly once with its reason
- [ ] The narration prediction was written before running, and every difference is explained
- [ ] No
std::vector; the count is kept in step with the fixed array - [ ] The 33rd reading and the invalid sensor are both refused, with messages
- [ ] An empty log's maximum is handled without an out-of-bounds read
- [ ] A
const DayLog&can be printed and searched - [ ] All ten test cases run, with output recorded
- [ ]
design-notes.mdcontains the member order, the derived-construction decision, and the temporary count
Related: Lab index · Module 3 — Constructors and Destructors · Previous: Practice Lab 5 · Next: Practice Lab 7 — Product catalogue with enforced invariants