Practice Lab 14 — Boarding queue: slicing reproduced and fixed¶
Difficulty: Intermediate · Estimated time: 4 hours · Module 7: Polymorphism and Virtual Functions
Object slicing is the failure that produces no error, no warning, and no crash — just a lost field. This lab makes you watch it happen and then remove the possibility.
At a glance¶
| Difficulty | Intermediate |
| Estimated time | 4 hours |
| Module | Module 7 — Polymorphism and Virtual Functions |
| Concepts practised | Base-class pointers and references, object slicing, copying polymorphic objects, containers of base values versus base pointers, override, virtual destructors |
| Prerequisite | Practice Lab 13 |
| Class design | yours — with the slicing failure specified |
Scenario¶
An airport boarding desk holds a queue of passengers. Every passenger has a name, a seat and a boarding group. Some have extras: a wheelchair requirement, a connecting flight, or a pet in the cabin — and those extras change the order in which they should board, and what the announcement says.
The desk's current code stores passengers in a vector of the base type, and it has spent a fortnight chasing a bug it described as "the wheelchair flag disappears after the passenger joins the queue". That description is a precise diagnosis of a specific C++ failure, and this lab is about it.
Problem statement¶
Implement:
- A
Passengerbase type with a name, a seat, a boarding group, and a virtualboarding_note(). - Three derived types carrying an extra field each, each overriding
boarding_note()and adding an operation that uses its extra field. - A boarding queue with operations to add a passenger, print the queue in boarding order, print the announcements, and count passengers by boarding group.
- A deliberate slicing demonstration: a function that takes
Passengerby value and prints the note, called with a derived passenger. Record what is printed and which fields are lost. - The fixed version: the queue stores passengers in a way that preserves their type, with a documented ownership rule and a virtual destructor.
- A copy demonstration: what happens when a derived passenger is copied into a base variable, and what the copy does with the derived fields.
In slicing-notes.md:
- The exact output of the sliced call, and the two fields it lost.
- Which of these four has the failure: a base variable assigned a derived object, a function taking the base by value, a container of base values, a container of base pointers. Answer each yes/no with a one-line reason.
- The rule you will follow from now on, in one sentence you could give a colleague.
Learning objectives¶
By the end of this lab you will be able to:
- Reproduce object slicing and explain it as "a copy of the base part only".
- Recognise each of the four places slicing can occur, and avoid them by design.
- Store polymorphic objects in a container with a correct ownership and destruction rule.
- Explain why a base class with virtual operations must not be passed by value, in terms of what is copied.
- Choose between owning pointers and references for a container, and state the cost of the choice.
Functional requirements¶
- Every passenger has a name, a seat and a boarding group; a boarding group outside
1..4is refused. - Boarding order is by group, then by a documented tie-break rule; the order is stable for equal keys.
- Each derived type's extra field is used: by the note, by the order, or by a query. A derived type whose extra field changes nothing is a design smell — say so if you find one.
- The announcements are one line per passenger, printed in boarding order.
- Counting by group is a single pass, and the counts sum to the queue size.
- The sliced call is present, marked in the source with a comment saying it is deliberately wrong, and its output is recorded in the notes.
- The queue's stored passengers are destroyed exactly once, and destroying them narrates the derived destructor.
- A passenger found by seat is returned without copying it.
- No
dynamic_castis needed anywhere; if you reach for one, record why. - An empty queue prints a clear message for each report.
Constraints¶
- Header per type family plus one source file; explicit build list.
- The queue's storage is your choice, with the ownership stated in writing. The sliced version must exist somewhere in the program, clearly labelled, so the failure is reproducible.
- No smart pointers (Module 11) — but you must state who deletes each passenger and prove the count.
- No templates in the queue (Module 12 will rewrite it as one).
- No warnings — which means the slicing case must not trip
-Werror; if it does, record the warning and say why you kept the code.
Starter task¶
In slicing-notes.md, before coding:
- Predict the output of the by-value call with a wheelchair passenger. Write the exact lines you expect.
- Predict the output of the same call with a base reference instead.
- Choose your queue's storage, and write the sentence that says who owns the passengers.
- Predict what
sizeofsays for the base and one derived type, and by how much they differ. Then measure it withsizeofand record both numbers.
Implementation tasks¶
- Implement the base and the three derived types with narration.
- Implement the queue with the ownership rule.
- Implement the reports and the boarding order.
- Implement the sliced call and record its output.
- Implement the fixed call, and print both side by side in
main. - Destroy the queue, count the narrations, and record that the count matches the number added.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Add a plain passenger, print | One line, base note |
| 2 | Add a wheelchair passenger, print | Derived note, mentioning the extra field |
| 3 | Print in boarding order with mixed groups | Sorted by group; tie-break as documented |
| 4 | Count by group | Counts sum to the queue size |
| 5 | Call the by-value function with a wheelchair passenger | The base note; the extra field is gone — output recorded |
| 6 | Call the reference version with the same passenger | The derived note |
| 7 | sizeof(Passenger) versus sizeof(WheelchairPassenger) |
Two numbers, recorded |
| 8 | Clear the queue | One derived narration per passenger; count matches |
| 9 | Find a passenger by seat | Returns without copying; no narration |
| 10 | Boarding group 5 at construction | Refused |
| 11 | Empty queue reports | Clear messages, no crash |
| 12 | Add a passenger and then add a copy | Two passengers, both destroyed once — or the copy is refused: your rule, stated |
Boundary cases to run deliberately: two passengers in the same group and the same seat letter; a queue of one; a derived passenger whose extra field is empty (for example, no connecting flight details).
Expected behaviour¶
boarding order:
group 1 Ayesha Khan seat 1A note: boarding with wheelchair assistance
group 2 Bilal Ahmed seat 7C note: connecting flight at 18:40
group 3 Zainab Iqbal seat 22F note: travelling with a pet
sliced call with a wheelchair passenger:
note: boarding
(the wheelchair requirement was sliced away)
by-reference call with the same passenger:
note: boarding with wheelchair assistance
sizeof: Passenger 64, WheelchairPassenger 72
clearing the queue: 3 destructor narrations for 3 passengers
Extension challenge¶
Add a priority boarding rule that depends on a derived type's extra field — a passenger with a connecting flight boards in group 1 regardless of their seat group.
Record in slicing-notes.md:
- Where the rule lives, and how it reads the extra field without a type check.
- Whether the rule belongs on the passenger (the passenger knows why it should board early) or on the queue (the queue owns the policy). Choose one, and give the rejected option's cost.
- What happens to the boarding order when two passengers both qualify, and whether your tie-break rule still holds.
- The change cost: files, operations, lines.
Debugging challenge¶
The queue silently loses data, and the code has a comment claiming it does not:
class BoardingQueue {
public:
void add(const Passenger& passenger) {
passengers_.push_back(passenger); // "stores a copy of the passenger"
}
private:
std::vector<Passenger> passengers_; // a vector of VALUES
};
Reproduce it — add a wheelchair passenger, print the queue — and answer:
- What exactly did
push_backcopy, and what was lost? - Why did the comment make the bug harder to find rather than easier?
- Which two changes fix it, and what does each one cost (an allocation, a lifetime, a destructor)?
- Which test case in the table catches it?
Reflection questions¶
- Where in your program is a derived object copied into a base variable, if anywhere? How did you check?
- Your queue stores pointers or references. What is the failure mode if a passenger is destroyed while still in the queue, and how would you prevent it?
- A base class with virtual operations should not be passed by value. Why is that a rule about the base, not about the derived types?
- Which of your derived types changed the boarding order, and which only changed the announcement? Is one of them a design smell?
sizeofdiffered by eight bytes. What would happen if a derived class's extra field were much larger than the base, and the object were sliced into a base array?- If a fifth kind of passenger arrived monthly, what would the change cost be in your design, and where would the extra field have to be handled?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ] The sliced call exists, is labelled as deliberate, and its output is in the notes
- [ ] The fixed call is shown beside it, and the difference is explained
- [ ] Four slicing contexts answered yes/no with reasons
- [ ]
sizeofrecorded for the base and a derived type - [ ] The queue's ownership rule is written down, and destruction narrations match the number added
- [ ] No
dynamic_cast— or the reason for one is recorded - [ ] All twelve test cases run, with output recorded
- [ ] Boundary cases run, including a queue of one and a derived passenger with no extra data
- [ ] The debugging challenge was explained before being fixed
- [ ] I can state the one-sentence rule that prevents slicing in my own code
Related: Lab index · Module 7 — Polymorphism · Previous: Practice Lab 13 · Next: Practice Lab 15 — Payment processing abstraction