Skip to content

Practice Lab 3 — Rectangle and geometry model

Difficulty: Beginner · Estimated time: 2–3 hours · Module 2: Classes and Objects

Your first class. Small enough to hold in your head, and it still manages to contain a decision that every larger class repeats: what is state, and what is computed?


At a glance

Difficulty Beginner
Estimated time 2–3 hours
Module Module 2 — Classes and Objects
Concepts practised Class syntax, public operations, data members, object interaction, multiple objects, computed versus stored values, const member functions
Prerequisite Practice Lab 2
Class design given — two classes, named for you

Scenario

A workshop cuts rectangular panels from sheet material. A quote has to say the panel's area, its perimeter, whether it fits inside a standard sheet, and what it costs — and the price list changes often enough that the workshop wants the arithmetic in one place.

You are given the two objects the workshop talks about: a Rectangle and a Sheet.


Problem statement

Implement Rectangle with a width and a height, and Sheet with a width, a height and a price per square metre. Both in one source file.

Operations:

Rectangle

  1. Construct from a width and a height.
  2. area() and perimeter().
  3. is_square() — true when the sides are equal.
  4. fits_inside(width, height) — true when the rectangle fits inside a given shape, allowing rotation (a 2×6 panel fits inside a 6×3 sheet).
  5. scaled(factor) — a new rectangle with both sides multiplied.
  6. describe() — a one-line description for the quote.

Sheet

  1. Construct from a width, a height and a price per square metre.
  2. area() and cost_of(const Rectangle&) — the material cost of cutting that panel from the sheet.
  3. fit_count(const Rectangle&) — how many panels fit in a simple grid layout, ignoring rotation.
  4. describe().

A main that quotes three panels and prints the results.


Learning objectives

By the end of this lab you will be able to:

  • Write a class with data members and member functions, and construct objects with a chosen initial state.
  • Decide for each piece of information whether it is stored or computed, and state the reason.
  • Mark the member functions that do not change the object const, and explain what would break without it.
  • Pass one object to another object's operation without copying it, and say who owns what.
  • Explain, in one sentence, which class owns the rule "a panel fits inside a sheet".

Functional requirements

  1. A rectangle with a non-positive width or height is rejected at construction with a message on std::cout and an object that reports itself as invalid — or by throwing, if you prefer; choose one and use it consistently for both classes.
  2. area and perimeter are correct for whole numbers and for fractional sides.
  3. is_square is true when the difference between the sides is zero.
  4. fits_inside returns true when the rectangle fits with or without rotation.
  5. scaled returns a new rectangle and does not modify the original — demonstrate this in the output.
  6. cost_of uses the sheet's price and the rectangle's area; it rounds the cost up to whole pence.
  7. fit_count is zero when the panel does not fit, and the whole-sheet grid count otherwise.
  8. describe for both classes is a single line, and the two are distinguishable in the output.

Constraints

  • One source file; g++ -std=c++17 -Wall -Wextra -Werror.
  • No public data members. The sides are private, with accessors where access is genuinely needed.
  • Every operation that does not change the object is const. The compiler will enforce that you meant it: call a non-const operation on a const object and read the error.
  • No friend, no inheritance, no std::function.
  • Money in whole pence as an integer; geometry in double.
  • No warnings.

Starter task

Before writing the classes, answer in design-notes.md:

  1. Which of these can be computed from the others: width, height, area, perimeter, whether it is a square? Which will you store, and why?
  2. fit_count has a rule — "ignore rotation, whole panels only, a simple grid". Where does that rule belong: in Sheet, in Rectangle, or in main? Write the sentence that decides it.
  3. A Rectangle of width 3 and height 4, and one of width 4 and height 3, have the same area. Are they equal? Would operator== be a good idea here, and what would it have to mean?

Then write the class declarations with no bodies, and check that no data member is public.


Implementation tasks

  1. Implement Rectangle, with the four computed operations and scaled.
  2. Implement Sheet, using Rectangle by reference in cost_of and fit_count.
  3. Write main, quoting three panels: one square, one tall, one that does not fit.
  4. Add a const Rectangle object to main and call area on it. Then remove const from area and read the compiler error, and paste it into design-notes.md.
  5. Run every test case and record the output.

Test cases

# Action Expected result
1 Rectangle(3, 4) area 12, perimeter 14, not a square
2 Rectangle(5, 5) area 25, is a square
3 Rectangle(2, 6).fits_inside(6, 3) true — it fits rotated
4 Rectangle(7, 2).fits_inside(6, 3) false
5 Rectangle(3, 4).scaled(2) a new 6×8; the original is still 3×4
6 Sheet(120, 240, 1500).cost_of(Rectangle(30, 40)) the area in square metres × 1500 pence, rounded up
7 fit_count for a panel larger than the sheet 0
8 Sheet(120, 240, 1500).fit_count(Rectangle(30, 40)) a whole-sheet grid count you can check by hand
9 Rectangle(0, 4) or Rectangle(-1, 2) rejected at construction, with a message
10 Calling area() on a const Rectangle compiles; the same call fails to compile if area is not const

Boundary cases to run deliberately: a rectangle exactly as large as the sheet; a rectangle of width equal to height; fractional sides such as 2.5 × 4.


Expected behaviour

$ ./panels
Quote for 3 panels
  panel 1: 30 x 40 cm, area 1200.0 cm2, perimeter 140.0 cm  [not square]
  panel 2: 50 x 50 cm, area 2500.0 cm2, perimeter 200.0 cm  [square]
  panel 3: 200 x 10 cm, area 2000.0 cm2, perimeter 420.0 cm  [not square]

sheet: 120 x 240 cm, 2.88 m2, 1500 pence/m2
  panel 1: cost 18 pence, fits 24 per sheet
  panel 2: cost 38 pence, fits 4 per sheet
  panel 3: cost 30 pence, fits 0 per sheet - does not fit
  panel 1 scaled by 2 is 60 x 80 cm, and the original is still 30 x 40 cm
  a const panel 1 reports area 1200.0 cm2

Extension challenge

Extend the sheet to support rotation in fit_count, so a panel that only fits turned is counted.

Then write, in design-notes.md:

  • How the count changed, with the example that proves it.
  • Why "fits with rotation" and "counts with rotation" are different questions, even though both are about the same two rectangles.
  • The operation you would add next for a real cutting plan, and what would make it hard.

Debugging challenge

The workshop reports that some panels are charged for and then rejected:

double cost_of(Rectangle panel) const {
    if (panel.area() <= area()) {          // fits?
        return price_per_m2_ * panel.area() / 10000.0;
    }
    return price_per_m2_ * area() / 10000.0;   // charge for the whole sheet instead
}

Reproduce it with a panel whose area is smaller than the sheet's but whose sides are longer, and answer:

  1. Which question did the fallback branch answer, and which question did it think it was answering?
  2. Why is "compare the areas" not the same test as "does it fit"?
  3. What is the smallest change that makes the operation honest about a panel that does not fit?

Reflection questions

  1. Which of your operations could have been a free function instead of a member? Which could not?
  2. fit_count lives on Sheet. What would have to be true for it to belong to Rectangle instead?
  3. What does const on a member function change about the objects you are allowed to call it on, and why does that matter for a container of sheets later?
  4. Two of your operations round: cost_of and the lengths. Where should rounding happen, and how many places did you have to decide it in?
  5. If the sheet's price changed, which objects would need updating, and which operations read the price?
  6. Which single change would make your two classes unusable by a caller — a private constructor, an operation no longer const, or a data member made public? Answer with what breaks in main.

Completion checklist

  • [ ] It compiles with -Wall -Wextra -Werror and no warnings
  • [ ] No public data members in either class
  • [ ] Every operation that does not change the object is const, and the const object in main proves it
  • [ ] scaled returns a new rectangle and leaves the original untouched — shown in the output
  • [ ] All ten test cases run, with output recorded
  • [ ] Boundary cases run, including a panel exactly the size of the sheet
  • [ ] design-notes.md states which values are stored and which are computed, with reasons
  • [ ] The compiler error from removing const is pasted into design-notes.md, verbatim
  • [ ] I can say which class owns the "does it fit" rule, in one sentence
  • [ ] I can name one operation I chose not to write, and why it was not needed

Related: Lab index · Module 2 — Classes and Objects · Previous: Practice Lab 2 · Next: Practice Lab 4 — Bank account as a type