Skip to content

Practice Lab 13 — Shape drawing system

Difficulty: Intermediate · Estimated time: 4–5 hours · Module 7: Polymorphism and Virtual Functions

The lab where the virtual keyword 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:

  1. A shape interface with the operations every shape must provide: area, perimeter, name, a description, and bounding_box. Take a position for each shape (a centre and a size) so the bounding box has something to compute.
  2. Circle, Rectangle, Triangle and RegularPolygon, each implementing the interface.
  3. 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.
  4. Dispatch demonstrations: the same operation called through a value, a pointer and a reference, and through a base pointer in a container.
  5. A virtual destructor 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

  1. Every shape's area and perimeter are correct for at least three sets of numbers, checked by hand in the test table.
  2. describe is one line, and includes the shape's name and its position.
  3. The canvas totals the area without any switch, if on a type, dynamic_cast or typeid.
  4. The overall bounding box is correct for any mix of shapes, including an empty canvas, which reports a documented "nothing" result.
  5. The largest shape by area is found without copying shapes, and a tie is resolved by a documented rule.
  6. A shape cannot be constructed with a non-positive size; each shape refuses with its own message.
  7. The interface declares a virtual destructor, and destroying shapes through base pointers narrates the derived destructor.
  8. RegularPolygon is final, and an attempted derivation from it does not compile; record the message.
  9. One shape operation is marked override in every implementation, and removing the base declaration produces a compile error rather than silence; record it.
  10. 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_cast on shape types anywhere.
  • No templates, no smart pointers (Module 11's exercise).
  • No warnings.

Starter task

In dispatch-notes.md, before coding:

  1. 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).
  2. 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.
  3. Predict what happens without virtual on the base operation, through a base pointer.
  4. Write the two lines that make a destructor virtual, and predict the narration difference without it.

Implementation tasks

  1. Implement the interface with its virtual operations and virtual destructor.
  2. Implement the four shapes with narration.
  3. Implement the canvas: add, describe, total area, bounding box, largest.
  4. Implement the five call forms in main, and fill in the table with actual output.
  5. Remove virtual from one operation, rebuild, run, and record the difference; restore it.
  6. Remove the virtual destructor, rebuild, run, and record the narration difference; restore it.
  7. 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:

  1. Which destructor ran, and which one should have?
  2. Why is the behaviour undefined in general, even though this run looked plausible?
  3. Which one word fixes it, where does it go, and why does the base's destructor not need to do anything else?
  4. Which test case in the table catches this?

Reflection questions

  1. Which of your interface operations is virtual and which is pure virtual? What decided each, and does the canvas care?
  2. Through a value, the call did not dispatch. Explain why in terms of what the compiler knows, in two sentences.
  3. override on every implementation: what did it catch while you were writing, if anything?
  4. RegularPolygon is final. Name a case where final is a mistake, and one where it is clearly right.
  5. Your polygon with 400 sides is nearly a circle. Is it still a polygon, and is that a modelling question or a mathematical one?
  6. 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 -Werror and no warnings
  • [ ] No switch, typeid or dynamic_cast on 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
  • [ ] override is used in every implementation, and the missing-base-declaration error is recorded
  • [ ] final on 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.md contains 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