C++ from the ground up

Capstone: a score report

A program that uses the whole course: records in a struct, a vector of them, an iterator walk, a sort with your own rule, a map that counts, and a formatted report.

Part 12 of 12

Advanced22 min readBack to Courses

The records and the report

The project is small and complete: three records, each a name, a team and a score. They go into a vector, the vector is sorted by score with the best first, and the program prints a report — the table, the total, the average to one decimal, and how many records share the top team.

Every piece of it comes from an earlier chapter: the struct and its constructor, the vector and push_back, a comparison function passed to sort, a format flag for the average, and a loop over the finished list. Nothing here is new, which is the point of a capstone — the pieces are what you learned, and the assembling is the skill.

Sorting by your own rule

sort with two arguments orders by the type own less-than. To order by score with the best first, pass a third argument: a function that answers whether one record should come before another. bool byScore(const Record& a, const Record& b) { return a.score > b.score; } reads as "a comes first when its score is greater", and the greater-than is what reverses the order.

The rule must be consistent — never say both that a comes before b and that b comes before a — or the sort can do surprising things. A simple comparison of one field always is.

Numbers that line up

An average of whole numbers is a fraction, and dividing two ints would throw the fraction away. The cast to double moves the division into the fractional world, and std::fixed with std::setprecision(1) prints exactly one digit after the point. Without those, 255 / 3 would print as 85 and the reader would be looking at a rounded guess.

Format flags stay set on the stream once you use them, so they affect every later number. Setting them once, right before the average, is the simple way to keep the rest of the report as whole numbers.

The shape of the capstone

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iomanip>

struct Record {
    std::string name;
    std::string team;
    int score;

    Record(std::string n, std::string t, int s)
        : name(n), team(t), score(s) {}
};

bool byScore(const Record& a, const Record& b) {
    return a.score > b.score;
}

int main() {
    std::vector<Record> records;
    records.push_back(Record("ada", "blue", 88));
    records.push_back(Record("bob", "red", 95));

    sort(records.begin(), records.end(), byScore);

    for (const Record& r : records) {
        std::cout << r.name << ' ' << r.score << '\n';
    }
    return 0;
}

Sort the records and print the report

Run it, then add a fourth record of your own and confirm it appears in the right place. Then count how many records scored above 80 with count_if, and print the name of the lowest scorer using back().

Output
Click "Run" to execute your code

Read the example top to bottom once before running it. The order is deliberate: the record type, the rule, the data, the sort, then the printing — and every program you write from here will have roughly that shape.

Checking a report before you trust it

A report is a program answer, and an answer can be wrong in ways that still look tidy. Check the count first: if it is zero, dividing by it is a problem, and front() on an empty vector is undefined. Check the total against the rows, and check that the average uses a double division rather than an integer one.

Formatting is part of correctness here. std::fixed and std::setprecision stay set on the stream, so a flag you set for the average also changes every later number. Set them where they apply, or save and restore the stream state; a report where one figure has a decimal and the rest do not is a report the reader has to decode.

The capstone shape is the shape of most small data programs: define the record, load the records, order them, then print. When something looks wrong, check those stages in that order rather than editing the printing first — the error is usually in the data or the rule, and the output is only where it became visible.

A report is also a test

Three records with known scores let you check the total by hand, and a fourth with a repeated team checks the counting. That is a small test suite written as data: if the program prints the right total for those four rows, the loop that adds them is right for any number of rows.

The order of those checks matters. Data first, then the rule, then the arithmetic, then the formatting: each stage depends on the one before it, so a wrong total makes every average after it wrong too. The earliest stage that disagrees with your expectation is the one to fix.

Three capstone mistakes: dividing by records.size() without checking it is non-zero, calling front() or back() on an empty vector, and leaving std::fixed set so later whole numbers print with a decimal tail. The compiler catches none of them; the count and the output are your tests.

Try these next

  • Add a team column to the report and total the scores per team with a map.
  • Print the lowest scorer as well as the highest.
  • Run the report with an empty record list and decide what it should print instead of crashing.

Team totals and averages

Run it and check each average against its total and count. Then add a fifth record on a new team, and print the team with the highest total by comparing the map entries.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What should the third argument to sort answer?