Sensor-Readings Toolkit
Sensor telemetry is everywhere in systems software: a coverage-guided fuzzer
accumulates branch-hit counts, a profiler tracks per-function instruction
counts, a load balancer logs request latency readings. All of those are
vectors of numbers at the core. In this lab you build the sensor-readings
toolkit — a small library of seven functions over std::vector<int> and
std::vector<double> — while touching every key std::vector idiom from
Chapter 16.
Every function in the toolkit is pure (arguments in, value out, no I/O)
so the grader can check exact answers. That separation also mirrors how the
chapter notes describe real CS6340 code: "if you do not need the index, do not
invent one" — and a pure function never invents one just because a for loop
is nearby.
Your tasks
totalReading(v)— Sum all readings. Use a range-for withconst auto&. Empty vector returns0(the loop body runs zero times automatically).averageReading(v)— Return the mean as adouble. Check for empty (return0.0). Cast before dividing:static_cast<double>(sum) / static_cast<double>(v.size()). Without the cast you get truncating integer division.maxReading(v)— Return the maximum element. Use an index-based loop withstd::size_tas the counter (for (std::size_t i{0}; i < v.size(); ++i)) so you practice the unsigned-index lesson. SeedmaxValwithv[0], update from index 1 onward. The assert guards the non-empty precondition.countAbove(v, threshold)— Count how many elements are strictly greater thanthreshold. Range-for +if. Empty vector →0(automatic).normalizeInPlace(v, scale)— Divide every element byscalein place. Usefor (auto& x : v)— the&is the whole lesson. Without it,xis a copy and every write is silently discarded.vis already a non-const ref.buildRamp(n)— Return{0, 1, 2, …, n-1}. Callreserve(n)first, then push each value withpush_back. Return the vector by value. Ifn <= 0return an empty vector.lastReading(v)— Return the last element using.at(v.size() - 1)(the safe, bounds-checked accessor). The assert already guarantees non-empty. Shows.at()vsoperator[]in concrete use.
Success criteria
totalReading({})— empty-vector edge (loop body must run zero times)averageReading({3, 4})— returns3.5, not3(the integer-division trap; missingstatic_castfails this check)maxReading({-10, -3, -7})— max of negative readings is-3countAbove({5, 10, 15, 20}, 5)—5is not strictly above5; only 3 passnormalizeInPlaceafter the call — values must have changed in the caller's vector (missing&inauto& xmeans they won't)buildRamp(0)/buildRamp(-5)— both return emptybuildRamp(100)— size must be exactly 100 (not 101, not capacity confusion)lastReading({1, 99})— returns99, not1(wrong-element trap)lastReading({99, 1})— returns1, not99(larger ≠ last)
Concepts practiced
New this chapter (Chapter 16):
std::vector<T>declaration, list-init, and subscript access (16.2)- Passing by
constreference (const std::vector<int>&) for read-only functions; passing by non-const reference for mutation (16.4) - Returning by value — efficient thanks to NRVO / move semantics (16.5)
std::size_tindexing in a count loop and the signed/unsigned warning (notes 16.3, 16.6, 16.7)- Range-based
forin the two forms this lab uses (notes 16.8 decision table):for (const auto& x : v)— read-only, no copyfor (auto& x : v)— mutate in place (the alias-vs-copy distinction)
push_backto grow a vector element by element (notes 16.11)reserve(n)to pre-allocate capacity and avoid repeated reallocations (notes 16.10, 16.11).at(i)vsoperator[]— bounds-checked vs unchecked access (notes 16.2, 16.3)
Reused from earlier chapters:
static_cast<double>(...)before integer division — the Ch 4 / Ch 10 trapassertfor programmer preconditions (Ch 9)- Header guard + namespace (Ch 2, Ch 7)
autofor type deduction in loop variables (Ch 10)- Function composition (Ch 2):
averageReadingcallstotalReadingrather than re-summing — a plain function call, not a template. (Both are ordinary non-template functions overstd::vector<int>.)
Constraints
Allowed: std::vector, range-based for, index for with std::size_t,
push_back, reserve, .at(), operator[], .size(), .empty(),
static_cast, assert, const auto&, auto&, return <vector> by value,
functions from earlier chapters.
Forbidden (not taught yet):
std::arrayor C-style arrays (Ch 17)- Explicit iterators
.begin()/.end()(Ch 18) <algorithm>(std::max_element,std::accumulate, etc.) (Ch 18) — the hand-rolled loops ARE the lessonnew[]/delete[](Ch 19)- Lambdas (Ch 20)
Required idioms:
- Task 3 must use an index-based loop with
std::size_t(sign lesson). - Task 5 must use
for (auto& x : v)— not a separate variable or index. - Task 6 must call
reservebefore thepush_backloop. - Task 7 must use
.at(), notoperator[].
Build & run locally
make # compile-check starter/sensor.cpp
make test # grade your code -> RED until TASK blocks are filled in
make test-solution # verify the reference (always green)
make solution # compile-check the reference implementation
make clean # remove build artifactsmake test is the grader. There is no interactive driver for this lab — the
functions are pure, and all the interesting behavior shows up in the test output.
Hints
Task 1 — range-for pattern for accumulation
int sum { 0 };
for (const auto& x : v) // const auto& = read each element without copying
sum += x;
return sum;const auto& is the right qualifier for reading elements of any type without
copying them (notes 16.8 decision table). For int the copy cost is tiny, but
building the habit here pays off on heavier types.
Task 2 — the cast-before-divide rule
if (v.empty()) return 0.0;
int sum { totalReading(v) }; // reuse Task 1
return static_cast<double>(sum) / static_cast<double>(v.size());The trap: 7 / 2 in C++ is 3 (truncates). You must cast to double before
the / operator runs. Casting the result afterward is too late — the truncation
has already happened. (Integer-division truncation is the Ch 4 / Ch 10 lesson —
the same trap as Ch 10's statkit — not a Chapter 16 topic.)
Task 3 — seeding max and the std::size_t loop
assert(!v.empty());
int maxVal { v[0] };
for (std::size_t i { 1 }; i < v.size(); ++i)
{
if (v[i] > maxVal) maxVal = v[i];
}
return maxVal;Start the loop at index 1 (you already used v[0] as the seed). Using
std::size_t for i matches v.size()'s type so there's no
signed/unsigned comparison warning (notes 16.3, 16.7 Option 1).
Task 4 — range-for with a conditional
int count { 0 };
for (const auto& x : v)
{
if (x > threshold) ++count;
}
return count;"Strictly greater than" means >, not >=. If the grader has {5, 10} and
threshold 5, only 10 passes the test; 5 > 5 is false.
Task 5 — auto& is the whole lesson
for (auto& x : v) // & makes x an alias for the real element
x /= scale;auto x (no &) would make x a copy. The division would happen on that
throwaway copy; the vector's storage would not change. The & makes x a
reference (an alias) for the actual element in memory. This is the Chapter 16.8
mutation pattern (notes decision table: auto& value → "mutate each element").
Task 6 — reserve then push_back
std::vector<int> result {};
if (n <= 0) return result;
result.reserve(static_cast<std::size_t>(n)); // capacity without length change
for (int i { 0 }; i < n; ++i)
result.push_back(i);
return result;reserve changes capacity (internal storage) but NOT length (size()
stays 0 after reserve). Only push_back / resize change the length. Confusing
the two is the classic bug: result.reserve(5); result[0] = 0; is UB because the
vector still has zero elements (notes 16.10).
Task 7 — .at() vs operator[]
assert(!v.empty());
return v.at(v.size() - 1);v.at(i) checks that i < v.size() at runtime and throws std::out_of_range
if not. v[i] skips that check — undefined behavior if i is out of bounds.
Here the assert already makes the index valid, so .at() is belt-and-suspenders;
the goal is to show the idiom explicitly (notes 16.2, 16.3).
Stretch goals
- Add a
minReadingthat mirrorsmaxReading(same pattern, different comparison). - Add a
medianReading— sort a copy ofvand return the middle element. Sorting requires<algorithm>(Chapter 18 — a preview). - Replace the
inttoolkit with a function templatetotalReading<T>so it works on bothstd::vector<int>andstd::vector<double>(Chapter 11/26 preview). - Wire
buildRampup to a real sensor device mock:buildRamp(1000)then runcountAbove(readings, threshold)to compute a simple threshold-crossing alert rate. This is essentially what a fuzzer's coverage-hit counter does: count how many branch edges exceeded a hit threshold. - Refactor
normalizeInPlaceinto a returningnormalizethat takes aconst std::vector<double>&and returns a new normalized vector — comparing the in-place vs copy-and-return tradeoffs (notes 16.5).
// Chapter 16 — Dynamic Arrays: std::vector · Sensor-Readings Toolkit (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the seven TASK blocks below. Each maps 1:1 to a task in the README
// and to a declaration in ../sensor.h. The bodies currently return PLACEHOLDERS
// so the file compiles immediately — that's why `make test` is RED right now.
// Your job is to turn it GREEN by implementing each function.
//
// make build compile your code (should already pass)
// make test grade it (RED until you fill these in)
// make test-solution verify the reference passes (always green)
//
// READING GUIDE — absorb the comments before writing code. Each block:
// • names the KEY CONCEPT from the notes,
// • calls out any TRAP (signed/unsigned, empty-vector edge, wrong loop style),
// • gives just enough of a HINT to get unstuck without spoiling the answer.
#include "../sensor.h"
#include <cassert> // assert — Chapter 9 precondition guard
namespace sensor
{
// ─── TASK 1: totalReading ─────────────────────────────────────────────────────
// KEY CONCEPT: range-based for loop (notes 16.8).
// Use: `for (const auto& x : v)` — `const auto&` avoids copying each element
// while making clear you only READ it. Accumulate into a running `sum`.
// EMPTY EDGE: return 0 — the loop body runs zero times (notes 16.8: "if the
// container is empty, the body runs zero times. No special case needed.")
//
// >>> YOUR CODE HERE <<<
//
int totalReading(const std::vector<int>& /*v*/)
{
return 0; // placeholder — correct only when v is empty
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: averageReading ───────────────────────────────────────────────────
// KEY CONCEPT: static_cast-before-divide (notes 10 / this chapter's tie-in).
// Dividing two integers in C++ does INTEGER DIVISION (truncates). Convert at
// least one side to double BEFORE dividing:
// static_cast<double>(sum) / static_cast<double>(v.size())
// SIGNED/UNSIGNED TRAP: v.size() returns std::size_t (unsigned). Dividing a
// double by a std::size_t is fine — the implicit conversion to double is safe
// here because size is always non-negative (notes 16.3).
// EMPTY EDGE: return 0.0 — documented behavior; check v.empty() first.
//
// >>> YOUR CODE HERE <<<
//
double averageReading(const std::vector<int>& /*v*/)
{
return 0.0; // placeholder — correct only when v is empty
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: maxReading ───────────────────────────────────────────────────────
// KEY CONCEPT: index-based loop with std::size_t (notes 16.3, 16.6, 16.7).
// Use: `for (std::size_t i{0}; i < v.size(); ++i)` — matching types avoids the
// signed/unsigned comparison warning. Start `maxVal` at v[0], then update.
// PRECONDITION: v must be non-empty — guard with assert (notes 16.4).
// WHY std::size_t HERE SPECIFICALLY? To practice the unsigned-index lesson.
// Notes 16.3 says: if you need the index, use std::size_t for the counter
// and index the vector with the same type. No cast needed.
//
// >>> YOUR CODE HERE <<<
//
int maxReading(const std::vector<int>& v)
{
assert(!v.empty()); // precondition: caller guarantees v is non-empty
return v[0]; // placeholder — always returns the first element
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: countAbove ───────────────────────────────────────────────────────
// KEY CONCEPT: range-based for loop with a conditional (notes 16.8).
// Use: `for (const auto& x : v)` — accumulate a count inside `if (x > threshold)`.
// EMPTY EDGE: loop body never runs -> count stays 0. No special case.
//
// >>> YOUR CODE HERE <<<
//
int countAbove(const std::vector<int>& /*v*/, int /*threshold*/)
{
return 0; // placeholder — correct only when v is empty or no elements > threshold
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: normalizeInPlace ─────────────────────────────────────────────────
// KEY CONCEPT: `auto&` (non-const reference) in range-for for MUTATION (notes 16.8).
// Use: `for (auto& x : v) { x /= scale; }`
// The `&` is CRITICAL: without it `x` is a copy and writes are discarded.
// The function signature already takes `v` by non-const reference so the
// mutation is visible to the caller.
// TRAP: if scale == 0.0, the divide is not a crash but a garbage result:
// floating-point division by zero is well-defined under IEEE-754 and yields
// inf or nan (it is INTEGER division by zero that is undefined behavior).
// A real toolkit would still guard scale==0; the tests here never pass it.
// EMPTY EDGE: loop body never runs -> no-op. No special case needed.
//
// >>> YOUR CODE HERE <<<
//
void normalizeInPlace(std::vector<double>& /*v*/, double /*scale*/)
{
// placeholder — does nothing (v is unchanged)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: buildRamp ────────────────────────────────────────────────────────
// KEY CONCEPT: push_back + reserve + return-by-value (notes 16.10, 16.11, 16.5).
//
// Step 1 — reserve(n): tell the vector how many elements are coming so it
// allocates once instead of repeatedly (notes 16.10):
// result.reserve(static_cast<std::size_t>(n));
// Step 2 — fill with push_back in a loop 0..n-1:
// for (int i{0}; i < n; ++i) result.push_back(i);
// Step 3 — return by value (move semantics / NRVO makes this efficient, 16.5).
//
// SIGNED/UNSIGNED: `n` is int; reserve takes std::size_t — cast before passing.
// The notes say: "cast at the boundary, not casually everywhere" (16.3).
// EMPTY EDGE: if n <= 0, skip the loop and return the (empty) vector.
//
// >>> YOUR CODE HERE <<<
//
std::vector<int> buildRamp(int /*n*/)
{
return {}; // placeholder — always returns an empty vector
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 7: lastReading ──────────────────────────────────────────────────────
// KEY CONCEPT: .at() for safe bounds-checked access (notes 16.2, 16.3).
// Use: `v.at(v.size() - 1)` — returns the last element OR throws if out of range.
// Compare: `v[v.size() - 1]` works too but is unchecked.
// SIGNED/UNSIGNED: v.size() returns std::size_t; subtracting 1 is safe here
// because the assert guarantees v is non-empty (so size >= 1).
// PRECONDITION: v must be non-empty — guard with assert.
//
// >>> YOUR CODE HERE <<<
//
int lastReading(const std::vector<int>& v)
{
assert(!v.empty()); // precondition: caller guarantees v is non-empty
return v[0]; // placeholder — returns first element, not last
}
// ─────────────────────────────────────────────────────────────────────────────
} // namespace sensor
Try the lab first — the learning is in the attempt.
// Chapter 16 — Dynamic Arrays: std::vector · Sensor-Readings Toolkit (SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// Reference implementation of ../sensor.h. Each function is complete, correct,
// and -Wall -Wextra clean. Peek here only AFTER taking a real swing at the
// TASK blocks — the learning is in implementing the vector idioms yourself.
//
// CS6340 lens: these patterns appear constantly in static-analysis tooling:
// range-for on coverage-point vectors, push_back to grow seed pools,
// maxReading-style scans over function-size statistics, normalizeInPlace-
// style passes that rewrite LLVM Value attributes in place.
#include "../sensor.h"
#include <cassert> // assert — Chapter 9 precondition guard
namespace sensor
{
// ─── TASK 1: totalReading — range-for (read-only) ────────────────────────────
// `const auto&` avoids copying each int (good habit for heavier element types;
// for int the cost is negligible, but the idiom is the lesson). The loop body
// runs zero times on an empty vector, so the return-0 base case is automatic.
int totalReading(const std::vector<int>& v)
{
int sum { 0 };
for (const auto& x : v) // KEY: range-for, const reference (notes 16.8)
sum += x;
return sum;
}
// ─── TASK 2: averageReading — static_cast-before-divide ──────────────────────
// The classic Chapter 4 / 10 trap: integer division silently truncates.
// `static_cast<double>(sum)` forces double arithmetic before the division
// happens. Dividing by v.size() (a std::size_t) is safe here because the
// implicit unsigned->double conversion loses nothing for realistic sizes.
double averageReading(const std::vector<int>& v)
{
if (v.empty()) // documented: empty vector -> 0.0
return 0.0;
int sum { totalReading(v) }; // reuse Task 1 (function composition)
return static_cast<double>(sum) // KEY: cast before divide (Ch 4 / Ch 10 trap)
/ static_cast<double>(v.size()); // v.size() is std::size_t (notes 16.3)
}
// ─── TASK 3: maxReading — index-based loop, std::size_t ──────────────────────
// We use an index loop deliberately (notes 16.3, 16.6, 16.7): std::size_t for
// the counter matches vector's size_type, so there is no signed/unsigned warning.
// Seeding with v[0] and comparing from index 1 onward is the clean pattern.
// The precondition (non-empty) is guarded with assert (notes 16.4 pattern).
int maxReading(const std::vector<int>& v)
{
assert(!v.empty()); // precondition: notes 16.4 says assert for programmer assumptions
int maxVal { v[0] }; // seed with the first element
for (std::size_t i { 1 }; i < v.size(); ++i) // KEY: std::size_t, no cast needed (notes 16.3)
{
if (v[i] > maxVal)
maxVal = v[i];
}
return maxVal;
}
// ─── TASK 4: countAbove — range-for + conditional ────────────────────────────
// Straightforward accumulator. The empty-vector case is automatic: the body
// never runs and count stays 0. Notes 16.8: "if the container is empty, the
// body runs zero times."
int countAbove(const std::vector<int>& v, int threshold)
{
int count { 0 };
for (const auto& x : v) // range-for: no index needed (notes 16.7, 16.8)
{
if (x > threshold)
++count;
}
return count;
}
// ─── TASK 5: normalizeInPlace — `auto&` range-for for MUTATION ────────────────
// The central lesson of this task (notes 16.8 decision table):
//
// `const auto& x` -> read only, no copy
// `auto& x` -> read + MUTATE in place
//
// Without the `&`, `x` would be a copy of the element — the division would
// happen on that throwaway copy and the vector's actual storage would be
// untouched. The `&` makes x an alias for the real element.
//
// `v` is passed by non-const ref so mutations persist for the caller (16.4).
void normalizeInPlace(std::vector<double>& v, double scale)
{
for (auto& x : v) // KEY: auto& (mutable alias) — not `auto` (copy) (notes 16.8)
x /= scale;
// Empty vector: loop body runs zero times; no-op. Correct by construction.
}
// ─── TASK 6: buildRamp — push_back + reserve + return-by-value ───────────────
// Three distinct vector idioms in one function:
//
// reserve(n) — allocates capacity upfront so the n push_backs below
// don't trigger repeated reallocations (notes 16.10, 16.11).
// It does NOT change the length — v.size() is still 0 here.
//
// push_back(i) — appends one element, increments length (notes 16.11).
//
// return result — returning a local vector by value is efficient: modern C++
// uses Named Return Value Optimization (NRVO) or move semantics
// to transfer storage rather than copying every element (16.5).
std::vector<int> buildRamp(int n)
{
std::vector<int> result {};
if (n <= 0) // edge case: non-positive n -> empty vector
return result;
result.reserve(static_cast<std::size_t>(n)); // KEY: reserve BEFORE push_back (notes 16.10)
// cast: n is int, reserve takes size_t (16.3)
for (int i { 0 }; i < n; ++i)
result.push_back(i); // KEY: push_back grows length one at a time (notes 16.11)
return result; // KEY: return-by-value is safe & efficient (notes 16.5)
}
// ─── TASK 7: lastReading — .at() safe subscript ──────────────────────────────
// Notes 16.2 / 16.3 contrast:
// v[i] — unchecked operator[]: undefined behavior if i >= v.size()
// v.at(i) — bounds-checked: throws std::out_of_range if i >= v.size()
//
// Here the assert already guarantees non-empty (so index size-1 is valid), but
// using .at() is belt-and-suspenders and shows the idiom explicitly. The
// subtraction is safe because size() >= 1 is ensured by the assert.
int lastReading(const std::vector<int>& v)
{
assert(!v.empty()); // precondition guard (notes 16.4 pattern)
return v.at(v.size() - 1); // KEY: .at() with bounds check (notes 16.2, 16.3)
}
} // namespace sensor
// Chapter 16 — Dynamic Arrays: std::vector · Sensor-Readings Toolkit (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// Tiny no-framework unit-test harness (drills/CLAUDE.md Style B spec).
// Includes ../sensor.h and calls the API through the sensor:: namespace.
// The Makefile links this file against starter/sensor.cpp for `make test`,
// and against solution/sensor.cpp for `make test-solution`.
//
// Each CHECK that fails prints its expression and line number; any failure ->
// non-zero exit -> `make test` is RED. When all pass: PASS ✅.
//
// Coverage strategy:
// • Normal inputs covering the main loop path.
// • EMPTY-vector edges — every function is tested on {} (the spec requires it).
// • Boundary values (single-element, negative, large n).
// • The static_cast-before-divide trap in averageReading (odd-sum case).
// • normalizeInPlace mutation persistence (alias-vs-copy trap).
// • buildRamp length and content correctness.
// • lastReading with .at() — verifies the correct element, not just v[0].
//
// IMPORTANT: maxReading and lastReading have a documented precondition
// (non-empty vector, guarded by assert). They are only called here with non-empty
// vectors to avoid triggering the assert as a false failure; the empty-vector
// behavior is documented as "caller's responsibility" in sensor.h.
#include <iostream>
#include <cmath> // std::abs for floating-point comparison
#include "../sensor.h"
static int fails = 0;
// Exact-equality check (for int-returning functions).
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
// Floating-point near-equality check (avoids rounding-dust false failures).
#define CHECK_NEAR(got, want) \
do { double g_ = (got), w_ = (want); \
if(std::abs(g_ - w_) > 1e-9){ \
std::cerr << "FAIL: " #got " ~= " #want " (got " << g_ << ", want " << w_ \
<< ") @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Task 1: totalReading — range-for sum ─────────────────────────────────
CHECK(sensor::totalReading({}) == 0); // EDGE: empty vector -> 0
CHECK(sensor::totalReading({5}) == 5); // single element
CHECK(sensor::totalReading({1, 2, 3}) == 6); // basic sum
CHECK(sensor::totalReading({10, 20, 30, 40}) == 100); // four elements
CHECK(sensor::totalReading({-3, -2, -1}) == -6); // negative readings
CHECK(sensor::totalReading({-5, 5}) == 0); // positive and negative cancel
CHECK(sensor::totalReading({100, 200, 300}) == 600);
// ── Task 2: averageReading — static_cast-before-divide ───────────────────
CHECK_NEAR(sensor::averageReading({}), 0.0); // EDGE: empty -> 0.0
CHECK_NEAR(sensor::averageReading({4}), 4.0); // single element
CHECK_NEAR(sensor::averageReading({1, 2, 3}), 2.0); // integer-friendly
// KEY TRAP: 7 / 2 == 3 (integer division), but 7.0 / 2 == 3.5. This CHECK
// catches the missing-cast bug that truncates the result.
CHECK_NEAR(sensor::averageReading({3, 4}), 3.5); // 7/2 = 3.5, NOT 3
CHECK_NEAR(sensor::averageReading({1, 3}), 2.0);
CHECK_NEAR(sensor::averageReading({10, 20, 30}), 20.0);
CHECK_NEAR(sensor::averageReading({1, 1, 1, 1, 1}), 1.0); // uniform
// ── Task 3: maxReading — index loop with std::size_t ─────────────────────
// Only non-empty vectors are tested here; non-empty is the documented
// precondition (assert-guarded in the implementation).
CHECK(sensor::maxReading({7}) == 7); // single element
CHECK(sensor::maxReading({3, 1, 4, 1, 5, 9}) == 9); // max at end
CHECK(sensor::maxReading({9, 3, 1, 4, 1, 5}) == 9); // max at front
CHECK(sensor::maxReading({3, 9, 1, 4, 5}) == 9); // max in middle
CHECK(sensor::maxReading({5, 5, 5}) == 5); // all equal
CHECK(sensor::maxReading({-10, -3, -7}) == -3); // all negative: -3 is max
CHECK(sensor::maxReading({0, 0, 1, 0}) == 1); // max among zeros
// ── Task 4: countAbove — range-for + condition ────────────────────────────
CHECK(sensor::countAbove({}, 5) == 0); // EDGE: empty -> 0
CHECK(sensor::countAbove({1, 2, 3}, 10) == 0); // none above threshold
CHECK(sensor::countAbove({1, 2, 3}, 0) == 3); // all above threshold
CHECK(sensor::countAbove({5, 10, 15, 20}, 10) == 2); // 15 and 20
CHECK(sensor::countAbove({5, 10, 15, 20}, 5) == 3); // strictly above 5: 10,15,20
CHECK(sensor::countAbove({5}, 5) == 0); // equal is NOT above
CHECK(sensor::countAbove({5}, 4) == 1); // just above threshold
CHECK(sensor::countAbove({-1, 0, 1}, -1) == 2); // 0 and 1 are above -1
// ── Task 5: normalizeInPlace — auto& mutation (the alias-vs-copy trap) ────
{
std::vector<double> v {};
sensor::normalizeInPlace(v, 2.0);
CHECK(v.empty()); // EDGE: empty -> no-op
}
{
std::vector<double> v { 4.0, 8.0, 12.0 };
sensor::normalizeInPlace(v, 4.0);
CHECK_NEAR(v[0], 1.0); // mutation persisted
CHECK_NEAR(v[1], 2.0);
CHECK_NEAR(v[2], 3.0);
}
{
// KEY TRAP TEST: if the loop uses `auto x` (copy) instead of `auto& x`
// (alias), the original vector is untouched. This check catches that.
std::vector<double> v { 10.0, 20.0, 30.0 };
sensor::normalizeInPlace(v, 10.0);
CHECK_NEAR(v[0], 1.0); // if this fails: missing `&`
CHECK_NEAR(v[1], 2.0);
CHECK_NEAR(v[2], 3.0);
}
{
std::vector<double> v { 1.0 };
sensor::normalizeInPlace(v, 2.0);
CHECK_NEAR(v[0], 0.5); // single element
}
// ── Task 6: buildRamp — push_back + reserve + return by value ────────────
{
auto r = sensor::buildRamp(0);
CHECK(r.empty()); // EDGE: n==0 -> empty
}
{
auto r = sensor::buildRamp(-5);
CHECK(r.empty()); // EDGE: negative n -> empty
}
{
auto r = sensor::buildRamp(1);
CHECK(static_cast<int>(r.size()) == 1);
// Only access r[0] when size is confirmed correct.
if (static_cast<int>(r.size()) == 1)
CHECK(r[0] == 0); // single-element ramp
}
{
auto r = sensor::buildRamp(5);
CHECK(static_cast<int>(r.size()) == 5);
if (static_cast<int>(r.size()) == 5)
{
CHECK(r[0] == 0);
CHECK(r[1] == 1);
CHECK(r[2] == 2);
CHECK(r[3] == 3);
CHECK(r[4] == 4); // {0,1,2,3,4}
}
}
{
// Large ramp — ensures reserve didn't confuse capacity with length.
auto r = sensor::buildRamp(100);
CHECK(static_cast<int>(r.size()) == 100);
if (static_cast<int>(r.size()) == 100)
{
CHECK(r[0] == 0);
CHECK(r[99] == 99);
}
}
// ── Task 7: lastReading — .at() bounds-checked subscript ─────────────────
// Only non-empty vectors; non-empty is the documented precondition.
CHECK(sensor::lastReading({42}) == 42); // single element
CHECK(sensor::lastReading({1, 2, 3}) == 3); // last of three
CHECK(sensor::lastReading({10, 20, 30, 40, 50}) == 50); // last of five
// KEY TRAP TEST: if the implementation returns v[0] instead of the last
// element, these catch it:
CHECK(sensor::lastReading({1, 99}) == 99); // not the first
CHECK(sensor::lastReading({99, 1}) == 1); // not the larger one
// ── Integration: chain tasks together ────────────────────────────────────
// Mimic a real sensor-pipeline: manually construct a known vector,
// compute its stats using the toolkit functions.
{
// {0,1,2,3,4,5}: sum=15, avg=2.5, max=5, countAbove(2)=3, last=5
std::vector<int> readings { 0, 1, 2, 3, 4, 5 };
CHECK(sensor::totalReading(readings) == 15);
CHECK_NEAR(sensor::averageReading(readings), 2.5); // 15/6 = 2.5
CHECK(sensor::maxReading(readings) == 5);
CHECK(sensor::countAbove(readings, 2) == 3); // 3,4,5 are above 2
CHECK(sensor::lastReading(readings) == 5);
}
{
// Verify buildRamp produces the same vector as {0,1,2,3,4,5}.
auto r = sensor::buildRamp(6);
if (static_cast<int>(r.size()) == 6)
{
// Reuse totalReading to cross-check the ramp sum (0+1+2+3+4+5==15).
CHECK(sensor::totalReading(r) == 15);
}
else
{
// Size wrong: count this as at least one failure.
CHECK(static_cast<int>(r.size()) == 6);
}
}
{
// Normalize a small vector and verify results.
std::vector<double> dv { 0.0, 2.0, 4.0, 6.0, 8.0 };
sensor::normalizeInPlace(dv, 2.0);
CHECK_NEAR(dv[0], 0.0);
CHECK_NEAR(dv[4], 4.0); // 8 / 2 == 4
}
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all sensor checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails
<< " check(s) failed -- fill in the TASK blocks in starter/sensor.cpp.\n";
return fails ? 1 : 0;
}
# Chapter 16 — Dynamic Arrays: std::vector · Sensor-Readings Toolkit · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. (Recipes use TABS.)
#
# Layout mirrors chapter-02 (the canonical Style B exemplar):
# sensor.h — declarations only (provided, complete)
# starter/sensor.cpp — TASK blocks for the learner to fill in
# solution/sensor.cpp — reference implementation
# tests/tests.cpp — the grader; includes ../sensor.h (found via -I.)
#
# -I. puts the chapter root on the include path so every file can write
# #include "sensor.h" by its simple name (mirrors ch 2 / LLVM convention).
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── Starter: compile-check the learner's sensor.cpp ──────────────────────────
build:
$(CXX) $(CXXFLAGS) -c starter/sensor.cpp -o starter/sensor.o
@echo "OK \xE2\x9C\x85 starter/sensor.cpp compiles. Now run: make test"
# ── Run: no interactive main in this lab, just print usage hint ───────────────
run: build
@echo "No interactive driver for this lab."
@echo "Use: make test (grade your solution)"
@echo " make test-solution (verify the reference)"
# ── Grade the LEARNER's code (RED until TASK blocks are filled in) ────────────
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/sensor.cpp -o tests/run
@./tests/run || echo "FAIL \xE2\x9D\x8C fill in the TASK blocks in starter/sensor.cpp until every check passes."
# ── Proof the exercise is solvable (MUST be green) ───────────────────────────
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/sensor.cpp -o tests/run
@./tests/run
# ── Peek at the reference (builds + prints it was built, no interactive run) ──
solution:
$(CXX) $(CXXFLAGS) -c solution/sensor.cpp -o solution/sensor.o
@echo "Reference solution compiled. See solution/sensor.cpp for the annotated implementation."
clean:
rm -f starter/sensor.o solution/sensor.o tests/run
rm -rf tests/run.dSYM
// ============================================================================
// sensor.h — PUBLIC INTERFACE of the sensor-readings toolkit (Chapter 16)
// ----------------------------------------------------------------------------
// This header is COMPLETE and PROVIDED. You do not edit it. It declares the
// seven functions that make up the toolkit; the bodies live in a separate
// .cpp file (starter/sensor.cpp) that you implement.
//
// Why a header? The same reason as Chapter 2's geo.h: the GRADER (tests/tests.cpp)
// must share a common contract with both your implementation and the reference.
// Change a signature here and nothing compiles. (CS6340 / LLVM lens: every LLVM
// pass you write follows exactly this pattern — Foo.h declares the API, Foo.cpp
// defines the bodies.)
//
// Header guard (Chapter 2): stops the file from being pasted in twice per build.
// ============================================================================
#ifndef SENSOR_H
#define SENSOR_H
#include <vector> // std::vector — the star of Chapter 16
// ─────────────────────────────────────────────────────────────────────────────
// All functions live in namespace sensor to match project naming conventions
// (Chapter 2 / 7 — namespaces keep names from colliding).
// ─────────────────────────────────────────────────────────────────────────────
namespace sensor
{
// ─── TASK 1: totalReading ─────────────────────────────────────────────────────
// Sum all readings in `v`. Return 0 for an empty vector (defined behavior:
// the sum of nothing is zero, by convention — state this in your mind before
// coding, and note it applies across ALL the statistics here).
//
// Pass-by-const-reference (notes 16.4): the caller's vector is NOT copied.
// Use a range-based for loop (notes 16.8): `for (const auto& x : v)`.
// Return type: int (readings are whole-number sensor counts).
int totalReading(const std::vector<int>& v);
// ─── TASK 2: averageReading ───────────────────────────────────────────────────
// Return the average of all readings as a double.
// IMPORTANT: return 0.0 for an empty vector (documented precondition).
//
// The static_cast-before-divide lesson from Chapter 4 / Chapter 10 applies:
// average = static_cast<double>(sum) / static_cast<double>(v.size())
// Without the cast, integer division would silently truncate (e.g. 7/3 == 2).
// This is the same trap as the Ch10 statkit grade-book.
double averageReading(const std::vector<int>& v);
// ─── TASK 3: maxReading ───────────────────────────────────────────────────────
// Return the maximum element in `v`.
// PRECONDITION (documented): v must be non-empty. Calling this on an empty
// vector is a programmer error; an assert guards it (Chapter 9 idiom).
// Use an index-based loop (`for (std::size_t i{0}; i < v.size(); ++i)`) so
// you can practice the SIGNED/UNSIGNED indexing lesson (notes 16.3).
int maxReading(const std::vector<int>& v);
// ─── TASK 4: countAbove ───────────────────────────────────────────────────────
// Count how many elements of `v` are strictly greater than `threshold`.
// Empty vector -> 0 (no elements to count; loop body never runs).
// Use a range-based for loop (notes 16.8).
int countAbove(const std::vector<int>& v, int threshold);
// ─── TASK 5: normalizeInPlace ─────────────────────────────────────────────────
// Divide every element of `v` by `scale` IN PLACE (mutating the vector).
// Empty vector: no-op (loop body never runs).
//
// KEY LESSON (notes 16.8): to MUTATE elements through a range-for, declare
// the loop variable as `auto& x` (a non-const reference):
// for (auto& x : v) { x /= scale; }
// Without the `&`, `x` is a copy and writes are silently thrown away.
// `v` is taken by (non-const) reference so mutations persist for the caller.
void normalizeInPlace(std::vector<double>& v, double scale);
// ─── TASK 6: buildRamp ────────────────────────────────────────────────────────
// Return a vector of `n` integers {0, 1, 2, ..., n-1} built with push_back.
// If n == 0, return an empty vector.
//
// This task demonstrates:
// 1. Constructing an empty vector and growing it with push_back (notes 16.11).
// 2. Using reserve(n) BEFORE the loop to avoid repeated reallocation
// (notes 16.10 / 16.11 — reserve changes capacity, not length).
// 3. Returning a vector by value — efficient thanks to move semantics
// or NRVO (notes 16.5).
// Return type: `std::vector<int>` returned by value (NOT an out-parameter).
std::vector<int> buildRamp(int n);
// ─── TASK 7: lastReading ──────────────────────────────────────────────────────
// Return the last element of `v` using the SAFE accessor `.at()`.
// PRECONDITION (documented): v must be non-empty. An assert guards it.
//
// `.at(index)` vs `operator[]` (notes 16.2, 16.3):
// v[i] — unchecked: undefined behavior on out-of-bounds index
// v.at(i)— checked: throws std::out_of_range on invalid index
// For learning/debugging, `.at()` is a helpful guard. For performance-critical
// inner loops the index validity is already known and `[]` is used.
// Here the precondition says non-empty; using .at() is belt-and-suspenders.
int lastReading(const std::vector<int>& v);
} // namespace sensor
#endif // SENSOR_H
make test locally
(see “Build & run locally” above).