Chapter 28 · Input and Output
Exercise · Chapter 28

Chapter 28 — Input and Output: The Report Engine

Streams are one of C++'s most elegant ideas: a single interface — <<, >>, getline — that works identically whether the bytes are going to the terminal, a file, or a string in memory. This lab makes that abstraction physical by building a "report engine": a small library that parses raw score lines, formats aligned table rows, assembles a full report, writes it to a real file, and reads it back — plus a utility that counts valid integers in a stream until it hits junk or EOF.

You implement five functions in starter/report.cpp:

  • Task 1 — parse a "name score" line with std::istringstream
  • Task 2 — format one aligned row with std::ostringstream + <iomanip>
  • Task 3 — assemble the full multi-line table
  • Task 4 — write the table to a real file and read it back
  • Task 5 — count valid integers in any stream until EOF/junk

Every function exercises a distinct stream concept; together they cover the full chapter. The CS6340 connection is direct: Lab 1 loads a seed corpus from a file (ifstream + getline), writes coverage results to another file (ofstream <<), and passes std::ostream& around so the same function works for both console and file output. You are building exactly those habits here.

Your tasks

  1. Parse a score line (std::istringstream + state check). parseScoreLine(line) creates a std::istringstream from the raw string, extracts the name token and double score with >>, checks the stream state, and validates the range [0.0, 100.0]. Returns a ScoreRecord with ok = true on success, ok = false on any failure (malformed input, out-of-range score). The key idiom is if (in >> name >> score) — using the extraction result as a boolean (notes 28.5).

  2. Format a table row (std::ostringstream + <iomanip>). formatRow(name, score) builds the aligned row "Ada 92.5" (name left-justified in a 10-char field; score right-justified in a 6-char field with 1 decimal) using an ostringstream and the manipulators std::left, std::right, std::setw, std::fixed, std::setprecision. The tests assert exact character-for-character equality — the tricky part is knowing that std::setw resets after each insertion while std::fixed and std::setprecision persist (notes 28.3).

  3. Assemble the full table (ostringstream). buildReport(records) writes a header row, a separator row, and one formatRow call per record into a single ostringstream, then returns .str(). Use formatRow for data rows; do not re-implement the alignment. An empty records vector produces only the 2-line header + separator.

  4. Round-trip through a real file (ofstream / ifstream). writeReport(path, report) opens an ofstream, checks if (!file) return;, writes the report string. readReportFirstLine(path) opens an ifstream, checks open success, reads the first line with std::getline, and returns it (notes 28.6). The tests write to tests/tmp_report.txt and verify the header line comes back correctly.

  5. Stream-state loop (while (in >> val)). countValidInts(in) counts integers extracted from any std::istream& until the first extraction failure (bad token or EOF). The canonical one-liner idiom: while (in >> val) ++count;. Do NOT call in.clear() — the function stops at the first failure, per the stream-state contract (notes 28.5).

Success criteria

  • parseScoreLine: valid lines, leading/trailing whitespace (>> skips it), boundary scores 0.0 and 100.0, out-of-range scores (101.0, −1.0), malformed input (missing score, non-numeric token), and empty string — all must return the correct ok flag.
  • formatRow: exact character-for-character string equality for names of varying length (3, 4, 10 chars), scores from 0.0 to 100.0 — the alignment must be pixel-perfect.
  • buildReport: exact 2-line header for empty input, presence of data rows from formatRow, and a full exact-string match for a 2-record report.
  • writeReport / readReportFirstLine: first line of the written file matches the expected header; non-existent file path returns ""; invalid write path does not crash.
  • countValidInts: all-valid stream (3), junk mid-stream stopping before the next valid int (1), junk at start (0), empty stream (0), negative ints (valid), 10 ints, junk at end (stops correctly), whitespace-only stream (0).
Concepts practiced
  • Stream hierarchyistream / ostream as the uniform base; cin, cout, ifstream, ofstream, istringstream, and ostringstream all descend from the same base (notes 28.1). Accepting std::istream& or std::ostream& in a function makes it stream-polymorphic.
  • std::istringstream — parse structured fields out of a std::string using the same >> operator you use on std::cin (notes 28.4)
  • std::ostringstream — build a formatted std::string without manual concatenation; retrieve it with .str() (notes 28.4)
  • <iomanip> manipulatorsstd::left, std::right, std::setw, std::fixed, std::setprecision for column-aligned table formatting; knowing which manipulators persist vs. reset (notes 28.3)
  • Stream statesgoodbit, failbit, eofbit; using the extraction result as a boolean (if (in >> name >> score)); the while-extraction idiom while (in >> val) (notes 28.5)
  • std::ifstream / std::ofstream — file I/O with open-success check (if (!file) return;), getline on a file, sequential read/write (notes 28.6)
  • Reused from earlier chapters: structs (Ch 13), std::vector / range-for (Ch 16), std::string manipulation (Ch 5), const-correctness (Ch 5)
Constraints
  • Allowed: std::istringstream, std::ostringstream, std::ifstream, std::ofstream, <iomanip> manipulators, std::getline, if (!stream) open checks, std::vector + range-for, std::string, struct ScoreRecord, while / for loops, if/else, double arithmetic and comparisons.
  • Required idioms: if (in >> name >> score) for simultaneous extraction + state check; while (in >> val) for the counting loop; if (!file) return; after every file open; out.str() to retrieve ostringstream contents.
  • Forbidden (not yet taught or not the point): raw C-style I/O (printf, scanf, fopen, fclose), manual string-padding with loops (use std::setw), std::stringstream where istringstream or ostringstream is specific enough (prefer the more specific type — it communicates intent), calling in.clear() inside countValidInts (it must stop at the first failure, not recover).
  • File paths: writeReport and readReportFirstLine use the path as given; the tests pass "tests/tmp_report.txt" which is relative to the chapter-28/ directory (the Makefile's working directory). Do not hard-code paths.
Build & run locally
shell
make            # compile-check starter/report.cpp (should work immediately)
make test       # grade your code  ->  RED until the TASK blocks are filled in
make solution   # run the grader against the reference (peek if stuck)
make clean      # remove build artifacts (including tests/tmp_report.txt)

make test is the grader — it calls every function across many inputs. There is no interactive make run for this lab (streams are the output; the grader shows everything you need).

Hints
Task 1 — istringstream construction and the if-extraction idiom
C++
std::istringstream in { line };          // "wrap" the string in an input stream
std::string name {};
double      score {};

if (in >> name >> score)                 // true if BOTH extractions succeeded
{
    if (score >= 0.0 && score <= 100.0)
        return ScoreRecord{ true, name, score };
}
return ScoreRecord{};                    // default has ok = false

std::istringstream is in <sstream>. Once constructed, it behaves exactly like std::cin but draws from the string. The chained extraction returns the stream, which evaluates to true only if no error flag is set (notes 28.5).

Task 2 — manipulator order matters; setw resets; fixed persists
C++
std::ostringstream out {};

out << std::left << std::setw(10) << name;                       // name field
out << std::right << std::fixed << std::setprecision(1)
    << std::setw(6) << score;                                    // score field

return out.str();

std::setw(N) applies only to the immediately following insertion and then resets to 0 (notes 28.3). std::fixed and std::setprecision(1) stay active for all subsequent floating-point insertions — harmless here since we output only one number per call.

Task 3 — header and separator widths
C++
std::ostringstream out {};

// Header: "Name" left in 10-char field, "Score" right in 6-char field
out << std::left  << std::setw(10) << "Name"
    << std::right << std::setw(6)  << "Score" << '\n';

// Separator: 9-dash name part, space, 6-dash score part
out << std::left  << std::setw(9) << "---------"
    << ' '
    << std::right << std::setw(6) << "------" << '\n';

// Data rows
for (const ScoreRecord& r : records)
    out << formatRow(r.name, r.score) << '\n';

return out.str();

The header uses setw(10) for name (same as formatRow) but only setw(6) for "Score" because the word is 5 chars — one padding space on the left. The separator uses setw(9) for the dashes (the name field is 10, but 9 dashes look cleaner) then a literal space before the 6-dash score divider.

Task 4 — ofstream write, ifstream first-line read
C++
void writeReport(const std::string& path, const std::string& report)
{
    std::ofstream file { path };
    if (!file) return;             // open failed — wrong path, permissions, etc.
    file << report;                // same << as std::cout, writes to disk
}

std::string readReportFirstLine(const std::string& path)
{
    std::ifstream file { path };
    if (!file) return "";          // open failed
    std::string line {};
    std::getline(file, line);      // reads up to (not including) the '\n'
    return line;
}

The destructor of ofstream flushes and closes the file — no explicit .close() needed. std::getline on an ifstream works identically to std::getline(std::cin, line) — same function, different stream kind.

Task 5 — the while-extraction idiom
C++
int count { 0 };
int val   {};
while (in >> val)   // extract; if it succeeds, val is updated and condition is true
    ++count;
return count;

When >> fails (bad token OR EOF), it sets failbit (and eofbit for EOF). The stream then evaluates to false in a boolean context — while exits. All subsequent >> calls on the same failed stream also fail immediately without changing val, so hitting one bad token stops the entire count. (notes 28.5)

Stretch goals
  • Make buildReport sort the rows by score (descending) before printing — requires std::sort + a lambda comparator (Chapter 18/20).
  • Add a parseReport(std::istream&) inverse: read a report file back into a std::vector<ScoreRecord> by parsing each line after the 2-line header — this deepens the ifstream + getline + istringstream combination.
  • Handle the >> / getline mixing trap (notes 28.2): write a readCsvLine function that reads a full CSV line with std::getline and then uses an istringstream to split on commas — a real-world pattern for seed-corpus files.
  • Write a countValidIntsWithRecovery variant that skips bad tokens and keeps counting (using in.clear() + in.ignore() — notes 28.5). Compare its behavior to countValidInts to see the state-management difference clearly.
  • Replace the file path std::string parameters with std::filesystem::path (C++17 — a preview of the filesystem library, formally beyond Ch 28).
starter/report.cpp C++
// Chapter 28 — Input and Output · Project: The Report Engine   (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the five TASK blocks below. Each maps 1:1 to a task in the README and
// to a declaration in ../report.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 stream operation.
//
//     make build         compile your code  (should already work)
//     make test          grade it           (RED until you fill these in)
//     make test-solution run the grader against the reference if you get stuck
//
// THE STREAM MINDSET: streams are buffers of characters with a read/write
// position and a set of STATE FLAGS. You write TO them with <<, read FROM them
// with >>, and always check whether the last operation succeeded.
//
// Key includes for this lab:
//   <sstream>  — std::istringstream, std::ostringstream  (notes 28.4)
//   <iomanip>  — std::setw, std::setprecision, std::left, std::right, std::fixed
//                                                          (notes 28.3)
//   <fstream>  — std::ifstream, std::ofstream             (notes 28.6)

#include "../report.h"

#include <fstream>   // std::ifstream, std::ofstream
#include <iomanip>   // std::setw, std::setprecision, std::left, std::right, std::fixed
#include <sstream>   // std::istringstream, std::ostringstream

// ─── TASK 1: parseScoreLine — std::istringstream extraction + state check ────
//
// Use std::istringstream to parse the two fields out of `line`.
// The stream abstraction lets you treat a std::string exactly like std::cin:
//
//     std::istringstream in { line };
//     std::string name {};
//     double score {};
//     if (in >> name >> score) { /* both fields extracted */ }
//
// After extracting name and score, validate:
//   - both extractions succeeded  (the if(...) above handles this)
//   - score is in [0.0, 100.0]   (a plain >= / <= check is fine)
//
// If everything is valid, return ScoreRecord{ true, name, score }.
// Otherwise return ScoreRecord{} (the default has ok=false).
//
// Hint: std::istringstream is declared in <sstream>. Create it on the stack
//       (stack allocation is fine — no heap needed for a string stream).
//
//   >>> YOUR CODE HERE <<<
//
ScoreRecord parseScoreLine(const std::string& /*line*/)
{
    return ScoreRecord{};   // placeholder — always reports parse failure
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 2: formatRow — std::ostringstream + <iomanip> ──────────────────────
//
// Build the formatted row in an std::ostringstream, then return .str().
//
// The manipulators you need (all from <iomanip>):
//   std::left          — left-align in the next field
//   std::right         — right-align in the next field (the default for numbers)
//   std::setw(N)       — field width for the NEXT inserted item (resets after use)
//   std::fixed         — use fixed-point notation (persists on the stream)
//   std::setprecision(N) — decimal places when std::fixed is active (persists)
//
// Target output (exact — the tests assert character-for-character):
//   formatRow("Ada",  92.5) == "Ada         92.5"   (Ada+7sp + 2sp+92.5 = 16 chars)
//   formatRow("Bob",  88.0) == "Bob         88.0"
//   formatRow("Zhao", 71.3) == "Zhao        71.3"
//
// Layout: name LEFT in 10-char field, score RIGHT in 6-char field, 1 decimal.
// That means: out << std::left << std::setw(10) << name
//                 << std::right << std::fixed << std::setprecision(1)
//                 << std::setw(6) << score;
// (Write them in this order — std::setw applies only to the NEXT item.)
//
//   >>> YOUR CODE HERE <<<
//
std::string formatRow(const std::string& /*name*/, double /*score*/)
{
    return "";   // placeholder — empty row
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 3: buildReport — assemble the table with an ostringstream ──────────
//
// Build the report in a single std::ostringstream:
//
//   1. Write the header row:
//        out << std::left << std::setw(10) << "Name"
//            << std::right << std::setw(6) << "Score" << '\n';
//
//   2. Write the separator row:
//        out << std::left << std::setw(9) << "---------"
//            << ' '
//            << std::right << std::setw(6) << "------" << '\n';
//      (9 dashes for the name column, space, 6 dashes for score column)
//
//   3. For each ScoreRecord `r` in `records`:
//        out << formatRow(r.name, r.score) << '\n';
//
// Return out.str(). An empty `records` vector produces only the 2-line header.
//
// TIP: manipulators like std::fixed and std::setprecision PERSIST on the stream.
// std::setw resets after each insertion. If the header/separator look wrong,
// check that your formatRow call hasn't left std::fixed active on the oss.
// (It has — and that's fine here, because std::setw(6) on a string still
//  right-pads to 6 chars regardless of fixed/float mode.)
//
//   >>> YOUR CODE HERE <<<
//
std::string buildReport(const std::vector<ScoreRecord>& /*records*/)
{
    return "";   // placeholder — empty report
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 4a: writeReport — std::ofstream ────────────────────────────────────
//
// Steps:
//   1. Declare: std::ofstream file { path };
//   2. Check:   if (!file) return;    // could not open -> silently bail
//   3. Write:   file << report;       // dump the whole report string
//
// The file is created (or truncated) automatically by std::ofstream. After the
// function returns, `file` goes out of scope and its destructor flushes + closes
// the underlying file — no explicit close() needed.
//
//   >>> YOUR CODE HERE <<<
//
void writeReport(const std::string& /*path*/, const std::string& /*report*/)
{
    // placeholder — does nothing (file is never written)
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 4b: readReportFirstLine — std::ifstream + std::getline ─────────────
//
// Steps:
//   1. Declare: std::ifstream file { path };
//   2. Check:   if (!file) return "";   // could not open
//   3. Read:    std::string line {};
//               std::getline(file, line);
//   4. Return:  line
//
// std::getline reads up to (but not including) the first '\n'. For a multi-line
// report, it returns exactly the header row — the lightweight round-trip check
// in the tests. (notes 28.2 — getline on an ifstream works identically to
// getline on std::cin.)
//
//   >>> YOUR CODE HERE <<<
//
std::string readReportFirstLine(const std::string& /*path*/)
{
    return "";   // placeholder — always returns empty (as if open failed)
}
// ─────────────────────────────────────────────────────────────────────────────

// ─── TASK 5: countValidInts — stream-state loop ───────────────────────────────
//
// The cleanest form: let the extraction attempt BE the loop condition.
//
//     int count { 0 };
//     int val {};
//     while (in >> val)   // true while extraction succeeds; false at EOF or junk
//         ++count;
//     return count;
//
// Why this works (notes 28.5): when >> fails (bad token OR EOF), it sets failbit
// (and eofbit at EOF). The stream then converts to bool as `false`, breaking the
// loop. Every subsequent >> on the SAME failed stream also returns false without
// extracting anything — so we stop at the first bad token, never skip it.
//
// Do NOT call in.clear() inside this function — the spec says to stop at the
// first failure, not recover. (Recovery would be a different function.)
//
//   >>> YOUR CODE HERE <<<
//
int countValidInts(std::istream& /*in*/)
{
    return 0;   // placeholder — always returns 0 (as if all extractions failed)
}
// ─────────────────────────────────────────────────────────────────────────────
Run
Submit
Run in your browser — coming soon For now: copy or download the files and use make test locally (see “Build & run locally” above).