Mini Project 5 — Shape Areas¶
Difficulty: Intermediate · Estimated effort: 4 hours ·
Prerequisites: Module 6 — Inheritance,
Module 7 — Polymorphism,
Module 8 — Abstract Classes ·
Combines: an abstract interface, virtual functions, polymorphic containers, unique_ptr
No complete solution is provided. The class list below is a starting point for your design, not a design.
1. Scenario¶
A sign-maker quotes jobs by wall area. A job is a list of panels — rectangles, circles and right triangles — and the quote is the total area plus the number of panels. The estimator currently copies each panel's dimensions into a spreadsheet and applies a formula chosen from a dropdown, which is exactly what polymorphism is for.
2. Problem statement¶
Build a small geometry library with an abstract Shape interface and concrete shapes
implementing it, so that a new shape can be added without editing the code that totals areas
or prints a report.
3. User requirements¶
- [ ] Add panels of several kinds to one job.
- [ ] Get the total area of the job and the number of panels.
- [ ] Print a report listing each panel with its own area.
- [ ] Be told when a shape's parameters are invalid rather than getting a nonsense area.
- [ ] Add a new shape later without touching the report or the total.
4. Functional requirements¶
- [ ] Abstract
Shapewitharea()andname()pure virtual, plusoperator<<ordescribe(). - [ ] Concrete
Rectangle,Circle,Triangle. - [ ] A job owning a
std::vector<std::unique_ptr<Shape>>. - [ ] Adding a panel takes ownership; the container destroys panels when it dies.
- [ ] A total-area function that takes the container and calls only the interface.
- [ ] Invalid dimensions (negative length, non-positive radius or height) refused.
5. Non-functional requirements¶
- [ ] A polymorphic base must have a virtual destructor; demonstrate that it does.
- [ ] No slicing anywhere: panels are held and passed by reference or smart pointer.
- [ ] No
new/deletein your code — construction goes throughstd::make_unique. - [ ] Builds warning-free with
g++ -std=c++17 -Wall -Wextra -Werror. - [ ] The area computations use a named constant for pi, defined once.
6. Suggested features¶
- Scale every panel in a job by a factor, returning a new job.
- Sort the report by area, largest first.
- A
Squareimplemented as a rectangle and a written note on whether that was wise. - Perimeter as a second interface function, with the shapes that cannot implement it questioned.
7. OOP concepts involved¶
| Concept | Where it appears |
|---|---|
| Inheritance | Each shape is a shape, and only for that reason |
| Abstract classes | Shape promises area(); the compiler enforces the promise |
| Polymorphism | The total and the report see one interface, many implementations |
| Virtual destructor | Deleting a panel through Shape& must run the derived destructor |
| Composition / ownership | The job owns its panels, via unique_ptr |
| Object slicing | Why panels are never stored by value — reproduce it, then avoid it |
8. Suggested design considerations¶
Ask whether inheritance is the right tool at all. A Circle and a Rectangle share an
interface, not an implementation. That is exactly the case for a pure interface — and it is why
there should be no data members in Shape.
Reproduce slicing on purpose. Put a Circle into a std::vector<Shape> (or pass one by
value) and watch what happens. Then write one sentence explaining the result. This is the single
most valuable minute of the project.
Keep pi in one place. A constexpr double pi in one header, or std::numbers::pi if your
toolchain provides it. Two pi constants that differ in the eighth digit is a real bug class.
Decide what a total costs. Your total loops over panels. Note what that costs if a job is later queried a thousand times, and whether caching belongs in the design or in the caller.
9. Minimum viable version¶
Shape with a virtual destructor and two pure virtuals; three concrete shapes; a job owning a
vector<unique_ptr<Shape>>; a total and a report; a main printing both.
10. Intermediate extension¶
Add a fourth shape without editing the total or the report, and count how many files you touched. Then add an invalid-dimension case to a shape and verify the refusal path is tested, not just written.
11. Advanced extension¶
Add job scaling and report sorting through the interface only. Then introduce a ShapeFactory
that turns a small text description ("rect 3 4") into a panel, and answer in writing whether
the factory removed or merely relocated the conditional logic that names each shape.
12. Testing checklist¶
- [ ] A rectangle of
3 × 4reports area12 - [ ] A circle of radius
1reports area within tolerance of pi - [ ] A triangle's area is a half base times height, checked at a boundary
- [ ] The total equals the sum of the individual areas printed in the report
- [ ] An empty job reports
0and0 panels - [ ] A negative dimension is refused with a message
- [ ] Destroying the job destroys every panel — verified with a destruction counter
- [ ]
deletethrough aShape*runs the derived destructor — verified the same way
13. Documentation checklist¶
- [ ] A one-line responsibility for each class
- [ ] Why
Shapehas a virtual destructor, stated where a reader will see it - [ ] The invariants of each concrete shape
- [ ] The slicing experiment written up: what happened, and one sentence of why
- [ ] Where pi lives and why there is only one
14. Self-evaluation rubric¶
| Criterion | Excellent looks like | Weight |
|---|---|---|
| Interface design | Shape is an interface with no state; adding a shape needs no edits elsewhere |
25% |
| Correctness | Areas verified including boundaries; empty job handled | 25% |
| Lifetime safety | unique_ptr throughout; virtual destructor demonstrated; no slicing |
25% |
| Failure handling | Invalid dimensions refused and tested | 10% |
| Documentation | Responsibilities, invariants and the slicing note written down | 15% |
Related: Mini projects · Project roadmap · All projects · Module 8 — Abstract Classes