Assignment 04: Classes and Operator Overloading

▶ Lab opening · Week 03 full screen
Note

Accessing your code. You may access your own private GitHub repository (see Getting the code below), clone it and open it in VS Code.

Plan to set aside about 3.75 to 4.25 hours. This is the longest assignment so far, and it is meant to be spread across the week rather than done in one sitting. findPositionFor (§9) and addLap (§10) are where most people slow down.


Ayrton Senna driving a red and white McLaren MP4/4 through standing water, seen from the front and slightly to the side. Spray plumes off the rear tyres and hangs in the air, blurring a Foster's advertising hoarding in the background. The car carries the number 12 and Marlboro, Honda and Shell markings, and Senna's yellow, green and blue helmet is visible in the open cockpit. A credit printed down the right-hand edge of the photograph reads Foto: Norio Koike - AGE.
Figure 1: Ayrton Senna in the McLaren-Honda MP4/4, 1988, the season he won the first of his three world championships. Photograph by the Instituto Ayrton Senna, via Wikimedia Commons, CC BY 2.0.

Ayrton Senna (1960–1994) was a Brazilian driver, three times Formula 1 World Champion, and the reason this assignment is about qualifying rather than about races. He took 65 pole positions in 161 starts, a record that stood until 2006, and his qualifying lap at Monaco in 1988, one and a half seconds faster than his own teammate in the same car, is still the one people reach for when they argue about the fastest single lap ever driven. He was killed in a crash at Imola in 1994. The leaderboard you are about to build is the screen he spent his career at the top of.

Objectives

By the end of this assignment, you will be able to:

  • Define a class split across a .h and a .cpp, and explain what each half is for
  • Choose what belongs in public and what belongs in private, and say why
  • Write a default constructor and an explicit-value constructor for the same class
  • Make a member immutable by giving it no setter, and explain when that is right
  • Mark a method const, and explain what breaks if you do not
  • Throw invalid_argument from a setter that is handed an illegal value
  • Format a number for display with setw, setfill and setprecision
  • Write a class that owns a fixed array and keeps it sorted as things are inserted
  • Split a method into private helper methods, and explain why that is easier to test
  • Overload operator<< as a free function returning ostream&
  • Write your own unit tests, before the code they test

These objectives map to course skills B1, B2, B3, and B5, all four of cluster B, which is assessed at Window 1 on Oct 23.

Note

What this assumes you have already met. A class split across a .h and a .cpp, public and private, constructors, getters and setters, and writing a unit test with Catch. If all of that is familiar, you can work straight through this page from §3 to §11 in the order it is written.

Two things this page teaches you as you need them, so do not skip past them: const on a getter, in §4, and overloading operator<<, in §12. If operator overloading is new to you, leave §12 until last. Nothing before it depends on it, and everything else works without it.

1. Introduction

A Formula 1 qualifying session is a leaderboard that updates itself. Drivers go out, set flying laps, and the timing screen keeps them in order, fastest at the top. It is the same problem as a high-score table, except for one detail that makes it much more interesting to build.

Only the top ten go through. Q2 ends with the ten fastest drivers advancing to Q3 and everyone else eliminated. So a board that holds exactly ten laps and throws away the eleventh is the sport’s own rule. When a driver sets a lap quick enough for P4, everyone below shuffles down one, and whoever was P10 is out.

You will build two classes:

  • Lap, one flying lap. Who set it, for which team, how long it took, and whether the stewards deleted it.
  • QualifyingSession, the timing screen. It owns an array of ten Lap objects and keeps them in order as laps arrive.

(F1 teams are officially called Constructors, and you are about to spend a week writing constructors. That is a coincidence and nothing in this assignment depends on it. The member is called myTeam.)

Tip

Everything you need to know about F1, in three sentences. Drivers set timed laps; the fastest lap wins, so a smaller number is better. Qualifying runs in knockout rounds, and only the ten fastest drivers make the final round. If a driver puts a wheel outside the white lines, the stewards delete that lap.

You do not need to follow it, and no check anywhere depends on knowing anything else.

Why a class at all?

In week 2 you wrote functions that took a grid, a row count and a column count, and passed all three around together everywhere. By the end, loadWorld had eight parameters. That is a lot!

A class is the fix. Lap bundles a driver, a team, a time and a flag into one thing you can pass as one argument, copy with one =, and put in an array. More than that, it lets you decide what the rest of the program is allowed to do with it, and that turns out to matter more than the bundling.

Here is the Python you already know, next to the C++ you are about to write:

Idea Python C++
Define a class class Lap: class Lap { ... };, note the semicolon
Constructor def __init__(self, driver): Lap(string driver);, named after the class
Data member self.driver = driver myDriver = driver;
“Do not touch this” _driver, a convention private:, enforced by the compiler
Printing def __str__(self): ostream& operator<<(...)
Method that reads only (no equivalent) string getDriver() const;

Look at the last three rows. Python asks you nicely not to touch _driver. C++ makes it a compile error. And const is a promise the compiler holds you to.

Tip

Open reference/Pair.h and reference/Pair.cpp in your assignment repository. It is the Pair class we built together in class, finished off, and Lap has exactly the same shape: a header full of promises, a .cpp full of Pair:: implementations, and an operator<< at the bottom. When you are stuck on syntax, look there first.

The version we actually typed in class is in the in-class repository, under w03-classes, with the Makefile and the tests we ran. It stops earlier than the one in your repository: no toString, no operator<<, and the getters are not const yet. Read it to remember what we did, and use reference/Pair.* as the model for Lap.


2. Getting the Code

Your private GitHub repository for this assignment is created for you inside the course organization, with the starter code already in it. There is nothing to accept and nothing to set up, if you are on the roster, it is there.

Clone it, replacing YOURUSERNAME with your GitHub username. If gh asks you to sign in, see step 9 of Getting Started:

gh repo clone 26fa-cs112/cs112-a04-YOURUSERNAME
cd cs112-a04-YOURUSERNAME
Note

Cannot find your repository?

If gh repo clone answers Could not resolve to a Repository, list the repositories you can see:

gh repo list 26fa-cs112

If yours is in the list, you mistyped the name. Copy it from the output and try again. Usernames that already contain a hyphen or a number are the easy ones to get wrong, because the pattern is cs112-<assignment>-<your username>.

If the list is empty, or the command errors, email me. The usual causes are an organization invitation you have not accepted yet (check your email, including spam and the Promotions tab), or a username I mistyped when I built the roster. Write to eric.araujo@calvin.edu and I will sort it out the same day.

While you wait, do not create a repository yourself and do not start the work somewhere else. Only the repository I create for you is connected to grading, and work done anywhere else has to be moved over by hand.

Open the folder in VS Code, then build it before you change anything:

make
make test

make should succeed. make test should report 3 / 40. Both of those are correct: the starter compiles, and almost nothing works yet. Those 37 failures are your to-do list, and they will tick over one at a time.


3. The Shape of the Repository

If the file is marked with ✎, you are expected to edit it. If it is not, read it, but do not change it.

Note

The TODOs are numbered by where they sit in the files, not by the order you do them. TODO 8 is near the top of Lap.cpp but it is the last thing on this page, because it is the one piece that uses operator overloading. Work in the order the sections run, §4, §5, §7, §8, and so on, and let the numbers fall where they fall.

File Yours? What it is
Lap.h ✎ The Lap class’s promises. You add the private data.
Lap.cpp ✎ The implementations. TODOs 2–6 and 8.
QualifyingSession.h ✎ The session’s promises. You add the private half.
QualifyingSession.cpp ✎ TODOs 9–12. Not compiled until you do §8.
tests.cpp ✎ Your own tests. Writing them is part of the assignment.
makefile ✎ Two lines near the top, in §8.
README.md ✎ Put your name at the top before you push (§14).
checks_lap.cpp The course’s checks on Lap. Read them.
checks_session.cpp The course’s checks on the session. Read them too.
main.cpp The demo program that prints a timing screen
laps.txt Fifteen laps. Ten will make the cut.
reference/Pair.* The Pair class from class, finished off
test.py The autograder. make test runs it, and so does the grader.
catch.hpp Catch2, the testing library, as one large header
catchmain.cpp Three lines that give Catch2 a main. Never edit it.
.gitignore Keeps compiled files out of git
Important

There are no hidden tests. Every check that decides your progress is in test.py and in the two checks_*.cpp files, in your repository, readable right now. What make test runs is exactly what the grader runs.

Read the checks before you write anything

Open checks_lap.cpp. It is about a hundred lines and it will take you five minutes, and it tells you precisely what Lap has to do, in a form the compiler can verify, which is more than any paragraph of English on this page can manage.

It is also the best model you have for §11, where you write tests of your own. Copy three of its habits:

  • one SECTION per behavior, named so that a failure tells you what broke
  • the boundary cases, not just the easy one, setTime(0) and setTime(-1), not only setTime(90.0)
  • REQUIRE_THROWS_AS for the refusals, so “it rejected that” is a tested behavior rather than something you hope happens

4. Lap, and What private Is For

A Lap holds four things:

Member Type Mutable?
myDriver string immutable, no setter
myTeam string immutable, no setter
myTime double (seconds) mutable
myDeleted bool mutable

Two of those get setters and two do not.

A lap was set by one driver, for one team. Nothing that happens later changes that, not a new time, not a deletion, not a pit stop. So there is no setDriver. In C++ you do not need a keyword to make a member immutable; you make it private and then decline to write a way to change it. Anyone who wants a different driver has to make a different Lap.

The time, though, changes constantly during a session, a driver sets one lap, then a better one. And the stewards can delete a lap minutes after it was set. Those two get setters.

Note

myDeleted is track limits. F1 cars have to keep four wheels inside the white lines; run wide and the stewards strike the lap off. It is a real mutable bool.

In this assignment, deleting a lap changes only how it prints. The lap keeps its position on the board, ranked on its time exactly as if nothing had happened, and the only difference is that operator<< adds the word DELETED after it. A deleted lap can sit in P1. Resist the urge to make setDeleted drop the lap off the board or push it to the bottom. That is a bigger design than this assignment needs, and no check asks for it.

TODO 1, the private section of Lap.h

Open Lap.h. All the prototypes are written for you; what is missing is the private: block. Declare the four members with the names and types in the table above.

The my prefix is a course convention. Inside a method it lets you tell a data member from a parameter at a glance, which matters more than you would think in a constructor whose parameter is called driver and whose member is myDriver.

TODO 2 and 3, the two constructors

A constructor has the same name as the class and no return type at all, not even void. It runs automatically whenever a Lap is created, and its job is to leave the object valid: no member holding whatever happened to be in that memory already.

Lap::Lap() {
    // set every member to its default
}

Lap::Lap(string driver, string team) {
    // store the two immutable fields; default the rest
}

Two functions with the same name is legal here, C++ picks by the arguments:

Lap a;                          // calls the first
Lap b("Leclerc", "Ferrari");    // calls the second

Defaults: empty driver, empty team, time 0.0, not deleted. A Lap exists before a time has been set for it, so the explicit-value constructor takes only two arguments.

TODO 4, the getters, and const

Four one-line methods. The interesting part is the word at the end:

string Lap::getDriver() const {

const after the parameter list is a promise that this method does not modify the object. Only const methods can be called on a const object, and the operator<< you write in §12 takes const Lap&, so if you leave const off a getter, the printing code will not compile, with an error message that will not obviously be about the getter.

Write it now so you don’t need to think about it again.


5. Refusing Bad Input

TODO 5, the setters

setDeleted is a one-liner.

setTime has a decision to make first. A lap time of zero or less is nonsense, and a class that quietly stores nonsense is worse than one that refuses:

#include <stdexcept>

void Lap::setTime(double seconds) {
    if (seconds <= 0) {
        throw invalid_argument("your message here");
    }
    // ... otherwise store it
}

Do not change the stored time when you reject a value; a rejected call should leave the object exactly as it was.

Tip

This is the week 2 idea one level down. Back then a broken file made loadWorld throw. Now a broken value makes a method throw. Same mechanism, same reason: the function cannot do what it was asked, so it says so rather than inventing an answer, and the caller decides what to do about it.

TODO 6, formatTime

Turn the stored double into what a timing screen shows:

Stored Printed
83.456 1:23.456
59.9 0:59.900
125.004 2:05.004
120.0 2:00.000

The minutes column is always there, seconds are always two digits, and there are always exactly three decimals, F1 times to the millisecond.

Build the string with an ostringstream, which behaves exactly like cout except that what you write lands in a string instead of on the screen. The whole method, with the two hard parts left for you:

// ── top of Lap.cpp, with the other includes ──────────────────
#include <sstream>          // ostringstream lives here
#include <iomanip>          // setw, setfill, setprecision live here

// ── further down, among the other Lap:: methods ──────────────
string Lap::formatTime() const {
    int    minutes = /* whole minutes in myTime */;
    double seconds = /* the seconds left over */;

    ostringstream out;      // an ostream that collects into a string
    out << minutes << ":" << /* the seconds part, padded */;
    return out.str();       // hand back what it collected
}

Both of those #include lines are already at the top of your Lap.cpp, so you do not have to add them. They are shown here so you know where ostringstream and the manipulators come from. If you ever write this method in a file of your own, those two lines are what make it compile.

Get the minutes with integer division, then subtract them off to find the seconds that are left. The seconds are the hard part: << on its own prints 5.004 when you need 05.004, and prints 5 when you need 5.000.

Manipulators, which are new. A manipulator is a thing you send into a stream with <<, exactly the way you send a number. The difference is that it prints nothing at all. Its whole job is to change how the things you send after it come out. They live in <iomanip>.

There are three jobs to do here, and one manipulator for each. Take them one at a time, and run each block as you read it, in a scratch .cpp file of your own rather than in the files you are handing in.

Job one: always three decimals.

Left to itself, C++ prints a double the way it thinks is tidiest, which is not what a timing screen wants:

out << 5.0;        // 5
out << 5.004;      // 5.004
out << 59.9;       // 59.9

Three times, three different shapes, and the first one has lost its decimals entirely. fixed says “plain decimal notation, always”, and setprecision(3) then means “exactly three digits after the point”:

out << fixed << setprecision(3) << 5.0;      // 5.000
out << fixed << setprecision(3) << 5.004;    // 5.004
out << fixed << setprecision(3) << 59.9;     // 59.900

Now all three have three decimals. 5.004 came out unchanged, because it already had three. A manipulator that appears to do nothing on one value is still doing the work on the others.

Job two: never fewer than two digits before the point.

0:5.000 is not a lap time; you want 0:05.000. That is setw, which says how wide the next thing printed has to be:

out << setw(6) << fixed << setprecision(3) << 5.0;    //  5.000   ← one space
out << setw(6) << fixed << setprecision(3) << 59.9;   // 59.900

5.000 was five characters, so setw(6) put one space in front. 59.900 was already six, so nothing happened.

setw(n) is a minimum, not a maximum. Ask for less room than the value needs and you simply get the value:

out << setw(2) << 12345;    // 12345, all five digits of it

Six is not a magic number here. It is the width of 05.004: two digits, a point, and three more.

Job three: pad with zeros, not spaces.

setw pads with spaces, and 5.000 is still not a lap time. setfill picks the character to pad with:

out << setw(4) << setfill('.') << 7;    // ...7
out << setw(4) << setfill('0') << 7;    // 0007

So setfill('0') is the last piece:

out << setw(6) << setfill('0') << fixed << setprecision(3) << 5.0;    // 05.000

All three together. Here is 125.004 done by hand, with its minutes and seconds written out as literals so you can see the shape of it:

out << 2 << ":" << setw(6) << setfill('0')
    << fixed << setprecision(3) << 5.004;      // 2:05.004

That is the pattern. What TODO 6 asks you to add is the part that is not in that line: working out the 2 and the 5.004 from myTime, instead of typing them.

The other three rows of the table go the same way. Check them by hand:

// 83.456   ->  minutes 1, seconds 23.456  ->  1:23.456
// 59.9     ->  minutes 0, seconds 59.9    ->  0:59.900
// 120.0    ->  minutes 2, seconds 0.0     ->  2:00.000

Look at the last two. 5.004 is the one that needed setfill, and 0.0 is the one that needed setprecision. No single lap time needs all three manipulators, but the method does.

Warning

Two things about manipulators that catch people out.

setw applies to the next item only. After one thing has been printed the width goes back to zero, so a setw before the minutes does nothing for the seconds. setfill, fixed and setprecision are the opposite: once set they stay set until something changes them.

setprecision means different things with and without fixed. On its own it counts significant figures; with fixed it counts digits after the point. You want the second. If you get 2:05 where you expected 2:05.004, this is why.

Run make test. With TODOs 1 to 6 done and nothing else, you should see 10 of 40. The only Lap checks still red are the two printing ones, which belong to §12, so Lap itself is finished. That is a natural place to pause, and §7 onward is the second half.


6. What Happens Next

Lap is done. The rest of the page builds the thing that holds ten of them, and it goes in this order:

Section What it adds
§7 the private half of the session, §8 the makefile the board exists and compiles
§9, §10 the constructor and the sorting laps arrive, and land in the right place
§11 your own tests you decide what else to check
§12 both operator<<s the board prints itself

§12 is the only one that needs operator overloading. If that is still new, leave it until last and do §7 to §11 first: they do not depend on it.


7. The Session’s Private Half

TODO 7, the private section of QualifyingSession.h

Two kinds of thing go below private: here.

The data:

Lap myLaps[MAX_LAPS];     // the board itself, ten Lap OBJECTS
int myNumLaps;            // how many of those slots are in use

Look hard at that first line. It is an array of ten Lap objects, not pointers, not references, and declaring it runs your Lap default constructor ten times, before a single lap has been added.

That is what a default constructor is for. An array of objects cannot exist without one: the array has to be made, so every element has to be made, so every element needs a constructor that takes no arguments. If you want to watch it happen, put a breakpoint in Lap::Lap() and press F5.

And the two helper methods:

int  findPositionFor(double time) const;
void shiftDown(int from);

These are private because they are machinery. Nobody outside the class should be shuffling the board by hand, the only thing the outside world gets to do is addLap, and the class decides what that means.

Tip

Splitting addLap into two helpers gives you two loops, each small enough to hold in your head and reason about alone, and addLap itself becomes about five lines that read like the rule they implement. When it goes wrong, and it will, you will be debugging one loop, not three tangled together.


8. Teaching make About a New File

Open the makefile. Near the top are two lines:

SOURCES = Lap.cpp

CHECKS  = checks_lap.cpp tests.cpp

make does not go looking for source files. It compiles what these lines name and nothing else, so QualifyingSession.cpp and checks_session.cpp are, at this moment, just text sitting on disk.

Add them. Space separated, and do not remove what is already there:

SOURCES = Lap.cpp QualifyingSession.cpp

CHECKS  = checks_lap.cpp checks_session.cpp tests.cpp

Now make test builds and runs the session checks too. Your score jumps a little, a couple of them pass against the stubs already, and then stops. The rest is §9 onward.

Warning

“undefined reference to…” is not a syntax error. If you add checks_session.cpp but forget QualifyingSession.cpp, you get a wall of messages like:

undefined reference to `QualifyingSession::addLap(Lap)'

That is the linker, not the compiler, and it means something called a function that was never built, not that you wrote it wrong. When you see “undefined reference”, check the makefile before you touch your code. It is the single most confusing C++ error message for beginners, and it almost always means a missing file rather than a missing brain cell.


9. Where Does This Lap Go?

TODO 9, the constructor

Shorter than you expect. Lap myLaps[MAX_LAPS]; has already default-constructed all ten laps by the time the constructor body starts, so the whole body is one line: set myNumLaps to 0, because no lap has been added yet.

Compare that with the version of this class CS112 built last spring, which stored plain int scores and had to loop over the array zeroing every slot. Objects that know how to initialize themselves save you that loop.

TODO 10, findPositionFor

Return the index where a lap of this time belongs, or -1 if it belongs nowhere.

Walk the ten slots with a for loop. At slot i there are three cases:

At slot i Why Answer
i >= myNumLaps slot i is empty, and it is inside the ten return i
time < myLaps[i].getTime() this lap is faster, so it goes above that one return i
neither that lap is faster; the board goes on keep looking

If the loop finishes without returning, the board is full of ten faster laps and this one is eliminated: return -1.

Important

The board sorts ASCENDING. The fastest lap is the smallest number and lives at index 0. Everything about this assignment runs the opposite way round from a high-score table, and your instinct will be wrong all week. “Faster” means “less than”.

Important

Ties. Two laps to the same thousandth is rare, but it happens, and the answer cannot be left to chance. Real F1 gives the position to whoever set the time first. So a new lap goes above an existing one only when it is strictly faster.

That is a single character. Compare with <, not <=. There is a check for it.


10. Making Room

TODO 11, shiftDown, then addLap

shiftDown(from) opens a gap at index from by moving everything below it down one place. Write it as a for loop, like findPositionFor, this is array-traversal practice, and there is a check for it. Whatever was in the last slot is pushed off the end of the board and is gone, not a bug, that is elimination.

Warning

Think about which end to start from. If you copy downward starting from the top of the array, you overwrite the very values you still have to move, and the whole board ends up holding the same lap ten times.

This is the same visit-order trap as a03’s falling sand, in three lines instead of thirty. If you are not sure which direction is right, do not guess, put a breakpoint in the loop and watch the array in the Variables panel.

Then addLap, which is short because the helpers did the work:

  1. ask findPositionFor where this lap goes
  2. if the answer was -1, the lap is eliminated, return, and change nothing
  3. shiftDown to open the gap
  4. put the lap in
  5. increase the count, but only if the board was not already full

Step 5 is the one people get wrong. When the board is already full, a lap arrives and a lap is pushed off the end, so the count does not move. Ten in, ten out.

TODO 11b, getLap and getNumLaps

getNumLaps is a one-line getter.

getLap(i) has to defend itself. myLaps always has ten slots, but only the first getNumLaps() of them mean anything, and reading past that is exactly the kind of array bug C++ lets you commit in silence. Throw invalid_argument if i is below 0 or at/above getNumLaps().


11. Your Own Tests

tests.cpp is yours. Tag every TEST_CASE you write with [mine], so that

./tester "[mine]"

runs your tests and nothing else. You need at least 4 test cases and 15 REQUIREs, and at least one must check that something illegal is refused with REQUIRE_THROWS_AS. That is a floor, not a target.

They also have to pass. A failing test of your own costs a check, the same as any other, either the code is wrong or the test is, and finding out which is the job.

A test earns its place by being able to fail for a reason you can name. Before writing one, finish this sentence: “this fails if I got ______ wrong.” If you cannot finish it, the test is not telling you anything.

Places to look, none of which the course checks cover:

  • a lap time that is very small, or very large
  • a lap added at each end of the board, and into the middle
  • the board at exactly nine laps, and at exactly ten
  • what a getter returns after a setter has been called twice
  • a deleted lap that is still fast enough for P1
Tip

Write the test before the code. It feels backwards for about ten minutes.

A test written afterwards tends to describe what you built, while a test written first describes what you meant to build, and only the second kind can tell you that you built the wrong thing. This is the week you try test-driven development.


12. Printing

TODO 8, operator<< for a Lap

One row: the driver, the team, the formatted time, and, only if the lap was deleted, the word DELETED. Use setw(), from TODO 6, to line the columns up. Because it applies to one item only, each column needs its own, and a column of names reads better with left in front of it:

out << left << setw(14) << lap.getDriver()
    << left << setw(12) << lap.getTeam();

The exact widths are yours; the checks look for the pieces, not the padding.

Two things make the signature look odd.

It is a free function, not a method. There is no Lap:: in front of it. The left operand of cout << lap is the stream, not the lap, and a method’s left operand is always its own object, so this simply cannot be a method on Lap.

It returns ostream&, a reference to the same stream it was handed:

ostream& operator<<(ostream& out, const Lap& lap) {
    // ... print things to out ...
    return out;
}

That return is what makes cout << a << b work. << groups left to right, so that line is really (cout << a) << b, the first call has to hand the stream to the second. A version returning void compiles fine on its own and then fails the moment anyone chains it. There is a check for exactly that.

A reference, not a copy, because streams cannot be copied.

Because it is not a method, it has no special access to private members, it goes through the getters.

Tip

reference/Pair.cpp has a worked operator<< at the bottom. It is four lines.

TODO 12, operator<< for the session

The timing screen:

 P  DRIVER        TEAM             TIME       GAP
 1  Verstappen    RedBull      1:16.483         -
 2  Norris        McLaren      1:16.512    +0.029
 3  Leclerc       Ferrari      1:16.604    +0.121

(Illustrative, those are not the laps in your laps.txt.)

For each lap on the board, in order:

  • the position, the index plus one. An array starts at 0; a leaderboard starts at 1.
  • the lap itself. You already wrote a Lap operator<< in TODO 8, so this is out << session.getLap(i). Do not print the driver and team again by hand, reuse what you built. And print the formatted time, never the raw double: a board showing 76.483 instead of 1:16.483 fails a check.
  • the gap to the leader, this lap’s time minus the time at position 0, to three decimals, with a leading +. P1 has no gap; print - instead.

Real timing screens show the gap rather than a raw time for everyone, because nobody watching cares that P7 did a 1:16.858, they care that it was six tenths off. Build the gap with an ostringstream the way you built formatTime, so setw() can line the column up afterwards.

This function is not a method either, so it works through the public getters, which is why getNumLaps() and getLap() are public and the helpers are not.

Now look at it

make qualifying
./qualifying

main.cpp reads fifteen laps out of laps.txt, hands every one of them to a QualifyingSession, and prints the board. Ten drivers survive.

Read main.cpp afterwards. It is about thirty lines and it never sorts anything, never compares two times, never touches an array. It hands laps over one at a time and asks the session to print itself. Everything that makes the board correct lives inside your class.


13. Testing Your Work

make test

Forty checks, in seven groups:

Group What it looks at
Lap (8) the checks in checks_lap.cpp
Class layout (4) your private sections, and that the immutables have no setters
QualifyingSession (12) the checks in checks_session.cpp
The helpers (4) both are private, and both use a for loop
Your build (3) your makefile compiles both check files; ./qualifying builds and runs
Your tests (3) see §11
More cases (6) values that appear nowhere in the checks files
Note

The 20 checks in the two checks_*.cpp files, the Lap and QualifyingSession groups above, do not run through the ./tester your makefile builds. make test compiles checks_lap.cpp and checks_session.cpp itself, straight against your Lap.cpp and QualifyingSession.cpp. Your ./tester is yours and you decide what goes in it, so it cannot be the thing that decides whether your classes are right. One check does look at your ./tester, and it is the one about your makefile.

That last group exists so that code which genuinely works passes and code tuned to the examples on this page does not. It is printed in test.py where you can read it.

Every failure comes with a hint. Work through them in order; the four TODO groups are independent enough that each one turns its own checks green as you go.

Tip

./tester needs a test’s full name, or a prefix with a * on the end. ./tester "Lap: format*" works; ./tester "Lap: format" matches nothing, prints almost nothing, and exits successfully, which looks a lot like passing.


14. Submit

Important

Before pushing, open README.md and add your name.

In VS Code, click the Source Control icon in the left sidebar, type a commit message, click the checkmark to commit, then sync. From the terminal:

git add .
git commit -m "Complete a04"
git push

Every push is checked automatically. Within a minute or two the Actions tab of your repository shows a run named Autograde. Open it: the job summary says how many checks passed, and names any that failed. Push as many times as you like; the most recent submission is the one that counts.