Module 7 — Lab 7: the motor pool¶
Levels: [L3 Modify → L6 Build] · Extends: Lab 6 · Time: 6–8 hours
The scenario¶
The university motor pool rents vehicles by the day: cars, motorbikes and a minibus. Rates differ per vehicle, the minibus has a per-day mileage allowance, and the director's office has noticed that the monthly invoice does not match the booking records — the motorbikes appear to be billed at the car rate.
The pool's software was written by inheriting a Vehicle class and putting the rate in
each subclass, then looping over std::vector<Vehicle>. It compiles. It is wrong in
two separate ways, and the invoice is where the two of them meet.
Your task is to build the loop that cannot be wrong, and to prove — by reproducing the old failure first — that you understand what was wrong with it.
The deliverable includes a failure you cause on purpose. Before you write the correct version, reproduce the slice and the static dispatch in a program that compiles and prints the wrong total, and record its output. That record is marked. It is also the only reliable way to learn what slicing looks like when it happens to you for real.
What you start from¶
examples/module-07/01-virtual-or-not/main.cpp— the same defect in miniature.examples/module-07/04-slicing/main.cpp— slices measured withsizeof, and the two fixes that work.- Your Lab 6 item family, if you want the catalogue to bill overdue charges the same way; the pattern transfers exactly.
Requirements¶
- A
Vehiclebase class with a name, a daily rate and a per-day mileage allowance. State in a comment, for each function, whether a subclass may replace it — and writevirtualwhere the answer is yes. The destructor rule applies to every base class here, whether or not you can think of a reason yet. - Three derived classes —
Car,Motorbike,Minibus— where the minibus adds something real: a per-day mileage allowance that changes the total. - A fleet that holds vehicles without slicing. Choose the ownership form
deliberately:
std::vector<std::unique_ptr<Vehicle>>,std::vector<Vehicle*>, or a container of values with a documented reason (there is one such reason, and if you take it you must say what is lost). - The broken version, preserved. A second program (or a
#ifdefbranch) that compiles, runs, and prints the wrong monthly invoice, with a comment naming both defects and the line each one is on. - One resource-owning derived class. Give
Minibusa member that allocates (a buffer of booked seats, or a telematics record list). Delete every vehicle through the base handle, and demonstrate — with a counter, asexamples/module-07/02-virtual-destructor/main.cppdoes — that nothing leaks. - The invoice loop, which takes the fleet and returns the total, and which works
correctly for every vehicle type without a single
switchor type check. - It must build with no warnings using the course's compile command:
g++ -std=c++17 -Wall -Wextra -Werror.
What to deliver¶
- A program that builds the fleet, prints each vehicle's daily total for a given number of days, and prints the monthly invoice for all three types.
- The recorded failure: the output of the broken version, with the two defects named and the lines they are on.
- An ownership note (five sentences): what the container owns, who may delete a vehicle, what happens to the invoices if a vehicle is removed mid-loop, and what would break if you changed the container to hold values.
- The lifetime trace for one resource-owning
Minibus: predicted before the run, showing construction and destruction through the base handle.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ] The correct version prints the right total for every vehicle type
- [ ] The broken version is preserved, and prints the wrong total, reproducibly
- [ ] Deleting through the base handle frees the
Minibus's resource — the counter returns to zero
It is correct
- [ ] No
switchon type, and nodynamic_cast, anywhere in the invoice path - [ ] Every function that must vary is
virtual; every function that must not be replaced is justified in a comment - [ ] The base destructor is virtual, and the predicted trace matches the real one
- [ ] Boundary cases work: an empty fleet, one vehicle, a zero-day rental, a minibus at exactly its mileage allowance
- [ ] The failure path is exercised: a vehicle that cannot be rented for the requested period is refused, and the invoice says so rather than silently skipping it
It is designed
- [ ] I can state each class's responsibility in one sentence
- [ ] Every parameter and container uses handles, never polymorphic objects by value
- [ ] The ownership note names who deletes what, and what the container guarantees
- [ ] The two defects in the broken version are named in terms of the static type of the expression and a copy of the static type — not "inheritance was broken"
It is verified
- [ ] I wrote the broken version before the correct one, and kept its output
- [ ] I predicted the construction and destruction order before running the trace
- [ ] I ran the leak counter, rather than reasoning that the destructor must be virtual
- [ ]
sizeof(Vehicle),sizeof(Car)and the container's element size are recorded, and I can say what each number shows
Common ways this lab goes wrong¶
| Symptom | Cause | What to do |
|---|---|---|
| The invoice is wrong only for motorbikes | dailyRate() is not virtual, so the base's rate wins through a Vehicle& |
Declare it virtual; then re-check that every varying operation is |
Every vehicle arrives as a Vehicle, and the extra data is gone |
The container holds objects, so push_back slices |
Hold unique_ptr<Vehicle> (or documented raw pointers), and pass handles |
error: … marked 'override', but does not override |
A signature differs from the base's — usually a missing const |
Match the base exactly; the note: line names the function |
| The minibus's buffer is never freed and no warning appears | The base destructor is not virtual | virtual ~Vehicle() = default;, then re-run the counter to prove it |
delete on the same vehicle twice, or a doubled invoice as the fleet is destroyed |
Two owners: the container and the caller both delete | One owner. Express it in the container's type, and say so in the ownership note |
| Adding the minibus's allowance required editing three functions | The design is still a tag in disguise | Move the calculation behind the interface; if it cannot move, that is a finding worth writing down |
| The broken version was "fixed" before it was recorded | The deliverable was skipped | Restore it from your own history, run it, and keep the output — the observation is the evidence |
| The trace shows eleven lines where you predicted seven | The Minibus owns a member that also traces |
Redraw the subobject map from Module 6; the order is your prediction's, not the compiler's error |
Reference solution¶
There is no published file for this lab, on purpose: it is a fleet, an interface and an
ownership decision, and the decisions are the work. What follows is the part the
module's examples do not show together — a polymorphic hierarchy that owns a resource,
a container that cannot slice, and the invoice loop that needs neither a switch nor a
type check.
// The parts to compare your version against.
class Vehicle {
public:
// Any class with virtual functions that may be deleted through this pointer
// needs this line, and needs it before you can think of a reason.
virtual ~Vehicle() = default;
Vehicle(std::string plate, long long dailyPaisa)
: plate_(std::move(plate)), dailyPaisa_(dailyPaisa) {}
const std::string& plate() const { return plate_; }
long long dailyPaisa() const { return dailyPaisa_; }
// Varies per vehicle, so it is virtual — and the invoice loop below
// depends on that, silently and completely.
virtual long long charge(int days) const { return dailyPaisa_ * days; }
virtual std::string describe() const { return plate_ + " (vehicle)"; }
private:
std::string plate_;
long long dailyPaisa_;
};
class Minibus : public Vehicle {
public:
Minibus(std::string plate, long long dailyPaisa, int allowancePaisa, std::size_t seats)
: Vehicle(std::move(plate), dailyPaisa),
allowancePaisa_(allowancePaisa),
seats_(new bool[seats]) {} // owns a buffer: the destructor's reason
~Minibus() override { delete[] seats_; } // only runs if ~Vehicle is virtual
long long charge(int days) const override {
return Vehicle::dailyPaisa() * days + allowancePaisa_; // the base's work, then ours
}
std::string describe() const override { return Vehicle::plate() + " (minibus)"; }
Minibus(const Minibus&) = delete; // Module 10's subject, stated early:
Minibus& operator=(const Minibus&) = delete; // a raw owning pointer must not be copied
private:
long long allowancePaisa_;
bool* seats_;
};
// One owner, expressed in the type. Nothing in this loop can slice, and nothing
// in it needs to know which kind of vehicle it is looking at.
long long monthlyInvoice(const std::vector<std::unique_ptr<Vehicle>>& fleet, int days) {
long long total = 0;
for (const auto& vehicle : fleet) {
total += vehicle->charge(days);
}
return total;
}
Four things to look for when comparing with your version:
- Whether the container can slice.
std::vector<Vehicle>cannot be made correct by any change toVehicle; the fix is the container. If you kept values, your ownership note must say what you gave up — and one honest answer exists: a fleet of one kind of vehicle has no need for polymorphism at all. - Whether the destructor's effect was measured.
~Minibus()freeingseats_is a claim; the counter inexamples/module-07/02-virtual-destructor/main.cppis the evidence. A lab that reasons "it must be virtual, so it must be fine" has not done the requirement. - Whether
charge()used the base's work or duplicated it.Vehicle::dailyPaisa()is used above rather than reading the private member — which is Module 4's rule surviving into a hierarchy, and it means a future change to how the rate is stored reaches the minibus too. - Whether the broken version's two defects are named separately. They are: a missing
virtual(the expression's type decides) and a container of objects (the copy's type decides). Fixing either one alone changes the number and leaves a bug — which is exactly what makes this lab worth doing rather than reading.
Next: Challenge problems — three open problems, including the memo on where not to use virtual dispatch. · All modules