Practice Lab 13 — Shape drawing system¶
Difficulty: Intermediate · Estimated time: 4–5 hours · Module 7: Polymorphism and Virtual Functions
The lab where the
virtualkeyword finally earns its place — and where you see, in one running program, the difference between an operation being hidden and being overridden.
At a glance¶
| Difficulty | Intermediate |
| Estimated time | 4–5 hours |
| Module | Module 7 — Polymorphism and Virtual Functions |
| Concepts practised | Virtual functions, dynamic dispatch through base pointers and references, overriding, override and final, virtual destructors, dispatch cost |
| Prerequisite | Practice Lab 12 |
| Class design | the shape set is given; the interface is yours to define |
Scenario¶
A drawing program keeps a canvas of shapes and can render it to a character grid, compute the total area and the bounding box, and export a description. Shapes are a circle, a rectangle, a triangle and a regular polygon with any number of sides.
The previous version stored every shape in a struct with a kind field and drew them with a switch.
Adding a shape meant editing the switch in four places, and one of the edits was forgotten — the bounding
box ignored regular polygons for two releases, and nobody noticed because the tests only covered circles.
Your job is the version that cannot forget.
Problem statement¶
Implement:
- A shape interface with the operations every shape must provide:
area,perimeter,name, a description, andbounding_box. Take a position for each shape (a centre and a size) so the bounding box has something to compute. - Circle, Rectangle, Triangle and RegularPolygon, each implementing the interface.
- A canvas holding shapes of mixed types, with operations to add a shape, render a description of all shapes, total the area, compute the overall bounding box, and find the largest shape by area.
- Dispatch demonstrations: the same operation called through a value, a pointer and a reference, and through a base pointer in a container.
- A
virtualdestructor on the interface, with a demonstration of what happens without one — destructor narration in every class makes this visible.
In dispatch-notes.md:
- The five call forms from point 4, with the output of each and a one-line explanation of which one dispatched dynamically and which did not.
- Where the dispatch actually happens, in terms of a vtable pointer, in three sentences you would say to a colleague.
- What the dispatch costs, in one measured comparison: one loop calling a virtual operation versus one calling a non-virtual operation, over a large number of iterations.
Learning objectives¶
By the end of this lab you will be able to:
- Declare and override virtual operations, and prove the override with
override. - Explain dynamic dispatch as "the operation chosen from the object's actual type at run time".
- Reproduce hiding: what happens when the base operation is not virtual.
- Explain why a base class with virtual operations needs a virtual destructor, and demonstrate the difference.
- Measure the cost of dispatch rather than guessing it.
Functional requirements¶
- Every shape's area and perimeter are correct for at least three sets of numbers, checked by hand in the test table.
describeis one line, and includes the shape's name and its position.- The canvas totals the area without any
switch,ifon a type,dynamic_castortypeid. - The overall bounding box is correct for any mix of shapes, including an empty canvas, which reports a documented "nothing" result.
- The largest shape by area is found without copying shapes, and a tie is resolved by a documented rule.
- A shape cannot be constructed with a non-positive size; each shape refuses with its own message.
- The interface declares a virtual destructor, and destroying shapes through base pointers narrates the derived destructor.
RegularPolygonisfinal, and an attempted derivation from it does not compile; record the message.- One shape operation is marked
overridein every implementation, and removing the base declaration produces a compile error rather than silence; record it. - Adding a fifth shape requires no edit to the canvas — demonstrate with the extension challenge.
Constraints¶
- Header per type family plus one source file; explicit build list.
- Shapes are owned by the canvas. For this lab, an array of raw owning pointers with an explicit delete loop is acceptable and instructive; Module 11 replaces it with smart pointers. Whatever you choose, say who deletes each shape and ensure the program does not leak — and record how you checked.
- No
switch/typeid/dynamic_caston shape types anywhere. - No templates, no smart pointers (Module 11's exercise).
- No warnings.
Starter task¶
In dispatch-notes.md, before coding:
- Write the interface's five operations, and mark which are virtual and which are pure virtual (you will justify the difference between those two choices in Module 8; for now, choose and note it).
- Predict, for the same call on the same derived object: through a value, through a base pointer, through a base reference. Write the predicted output for each into the table you will fill in later.
- Predict what happens without
virtualon the base operation, through a base pointer. - Write the two lines that make a destructor virtual, and predict the narration difference without it.
Implementation tasks¶
- Implement the interface with its virtual operations and virtual destructor.
- Implement the four shapes with narration.
- Implement the canvas: add, describe, total area, bounding box, largest.
- Implement the five call forms in
main, and fill in the table with actual output. - Remove
virtualfrom one operation, rebuild, run, and record the difference; restore it. - Remove the virtual destructor, rebuild, run, and record the narration difference; restore it.
- Run the comparison loop from point 3 of the notes and record both timings.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Circle, radius 10, area | Your documented value of π, printed to a stated precision |
| 2 | Rectangle 4×6 | area 24, perimeter 20 |
| 3 | Triangle with sides 3, 4, 5 | area 6, perimeter 12 |
| 4 | Regular polygon, 6 sides, side 2 | The exact formula's result, checked by hand |
| 5 | Canvas with one of each | Total area equals the sum of the four individual areas |
| 6 | Bounding box of all four | Contains every shape; verified by a check printed in the program |
| 7 | Largest shape with a tie | The documented tie rule |
| 8 | Empty canvas bounding box | The documented "nothing" result, no crash |
| 9 | Call through the value | The base implementation or the derived one — record which, and why |
| 10 | Call through a base pointer, with virtual |
The derived implementation |
| 11 | The same call without virtual |
The base implementation; the difference is the lab's lesson |
| 12 | Destroy shapes through base pointers | Derived destructors narrate; without the virtual destructor they do not |
| 13 | Non-positive size at construction | Refused by each shape |
| 14 | Derive from RegularPolygon |
Does not compile; the message is recorded |
Boundary cases to run deliberately: a polygon with 3 sides and with 400 sides; a degenerate triangle (sides that cannot form a triangle) — decide whether it is refused, and record the rule.
Expected behaviour¶
canvas with 4 shapes
circle at (10, 10) r=5 area 78.54 perimeter 31.42
rectangle at (0, 0) 4x6 area 24.00 perimeter 20.00
triangle at (2, 2) 3,4,5 area 6.00 perimeter 12.00
polygon at (5, 5) n=6 s=2 area 10.39 perimeter 12.00
total area: 118.93
bounding box: (0, 0) to (15, 15) [contains every shape: yes]
largest: rectangle (24.00)
through a value : shape
through a base ptr : circle
without virtual : shape <- hiding, not overriding
destroying through base pointers:
[dtor] circle
[dtor] rectangle
...
Extension challenge¶
Add a fifth shape — an ellipse, or a composite shape made of two others — and report the change cost.
Record in dispatch-notes.md:
- The files touched, as a number, and whether the canvas needed any edit at all.
- Whether a composite shape (one that holds other shapes and computes its area from theirs) was harder than a simple one, and what that difficulty says about the interface you chose.
- Whether the interface would have needed to change for a shape with no perimeter (for example, an open path). If your interface cannot express it, say what the alternative design is.
Debugging challenge¶
The canvas leaks memory and destroys the wrong destructor:
class Shape {
public:
virtual double area() const = 0;
~Shape() { std::cout << "[dtor] shape\n"; } // not virtual
};
void Canvas::clear() {
for (Shape* shape : shapes_) {
delete shape; // "deletes the shape"
}
shapes_.clear();
}
Reproduce it — put a derived shape with a member that narrates into the canvas, clear it, and count the narrations — and answer:
- Which destructor ran, and which one should have?
- Why is the behaviour undefined in general, even though this run looked plausible?
- Which one word fixes it, where does it go, and why does the base's destructor not need to do anything else?
- Which test case in the table catches this?
Reflection questions¶
- Which of your interface operations is virtual and which is pure virtual? What decided each, and does the canvas care?
- Through a value, the call did not dispatch. Explain why in terms of what the compiler knows, in two sentences.
overrideon every implementation: what did it catch while you were writing, if anything?RegularPolygonisfinal. Name a case wherefinalis a mistake, and one where it is clearly right.- Your polygon with 400 sides is nearly a circle. Is it still a polygon, and is that a modelling question or a mathematical one?
- What did your dispatch measurement show, and what does that number mean for a program that dispatches a million times a second?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ] No
switch,typeidordynamic_caston shape types — searched for and recorded - [ ] The virtual destructor is present, and the difference it makes to narration is recorded
- [ ] Hiding was reproduced (non-virtual base operation) and its output recorded
- [ ]
overrideis used in every implementation, and the missing-base-declaration error is recorded - [ ]
finalon the polygon refuses derivation, and the message is recorded - [ ] No leaks: the ownership rule is written down and checked
- [ ] All fourteen test cases run, with output recorded
- [ ]
dispatch-notes.mdcontains the five call forms, the vtable explanation and the measured cost - [ ] The debugging challenge was explained before being fixed
Related: Lab index · Module 7 — Polymorphism · Previous: Practice Lab 12 · Next: Practice Lab 14 — Boarding queue: slicing reproduced and fixed