the `geo` mini-library
You're completing a tiny geometry library, geo, that other programs can link
against to compute areas, perimeters, hypotenuses, and diagonals of basic
shapes. The library's public interface already exists — a header,
geo.h, that declares five functions inside namespace geo behind a
header guard. Your job is to write the bodies in
starter/geo.cpp. A tiny program (demo.cpp) and
the grader (tests/tests.cpp) both #include "geo.h" and call
your functions through geo:: — they never see how the functions work, only what
the header promises.
That split is the entire point of the chapter. Chapter 1 lived inside one
main(); here you practice the real shape of a C++ program: declarations in a
header, definitions in a .cpp, everything namespaced, compiled as
multiple files and joined by the linker. This is also the exact file
layout of every LLVM pass you'll write in CS6340 — a Foo.h that declares an API
inside a namespace, and a Foo.cpp that defines it. You're building that muscle
on something you can check by hand.
Your tasks
rectangleArea(width, height)— returnwidth * height. Both areint; the product isint. (Warm-up.)rectanglePerimeter(width, height)— return the distance around all four sides:2 * width + 2 * height.rightTriangleArea(a, b)— return half the bounding rectangle:(a * b) / 2, as adouble. Mind the types: for an odd product like3 * 5 = 15the answer is 7.5, not 7. Make the division happen in floating point (e.g. divide by2.0) so the half isn't chopped off by integer division.hypotenuse(a, b)— returnstd::sqrt(a*a + b*b)(Pythagoras).<cmath>is already included for you.rectangleDiagonal(width, height)— the diagonal equals the hypotenuse of the right triangle whose legs are the rectangle's sides. Compose: call your ownhypotenuse(width, height)and return its result — don't re-derive the square root. (Insidenamespace geo, call it unqualified.)
Success criteria
- a zero-width rectangle (area
0; perimeter is just the two heights), - the odd-product right triangle
3, 5→ 7.5 (catches the integer-division trap in Task 3), - a cross-check that
rectangleDiagonal(w, h) == hypotenuse(w, h)— proving Task 5 really delegates instead of re-deriving.
Concepts practiced
- User-defined functions: definitions, parameters, and return values (2.1, 2.2, 2.4)
- The header / source split — declarations in
geo.h, definitions ingeo.cpp(2.8, 2.11) - Forward declarations (prototypes): the header is a set of them (2.7)
- Header guards (
#ifndef / #define / #endif) (2.10, 2.12) - A user-defined namespace (
namespace geo) and the scope-resolution operator::(2.9) - Functions calling functions —
rectangleDiagonalreuseshypotenuse(2.1, 2.6, DRY) - Reused from Chapter 1: variable initialization with
{}, expressions,std::cout,int - A first taste of
doubleandstd::sqrt(kept to whole-number answers on purpose)
Constraints
- Allowed:
intanddouble; the four arithmetic operators;std::sqrtfrom<cmath>; calling your owngeofunctions. - Required idioms: keep every body inside
namespace geo { ... }so its full name matches the prototype ingeo.h; leave#include "geo.h"at the top ofgeo.cpp(so the compiler checks your bodies against the contract). - Forbidden (not taught yet):
if/branches (Ch 4/8), loops (Ch 8), classes,std::stringscanning. You don't need any of them. - Don't put a function definition in
geo.h, and don't#includeany.cppfile — the Makefile adds the right.cppto the build instead (2.8, 2.11). - Task 5 must reuse
hypotenuserather than callingstd::sqrta second time.
Build & run locally
make # compile demo.cpp + starter/geo.cpp -> starter/app
make run # run the demo (numbers are wrong until you finish the tasks)
make test # grade your geo.cpp against the unit tests <-- the real goal
make solution # build + run the reference library's demo
make cleanThe grader compiles tests/tests.cpp together with your starter/geo.cpp
into one program and runs it — that two-file compile, joined by the linker, is
the chapter's whole idea.
Hints
Task 1 & 2 — the rectangle functions
These are one-liners. return width * height; and return 2 * width + 2 * height;.
The placeholders currently return width + height; (addition) on purpose — that's
why area and perimeter come out wrong.
Task 3 — why (a * b) / 2 gives the wrong answer
a and b are int, so a * b is an int, and int / int does integer
division — it throws away the remainder before the value ever becomes a
double. 15 / 2 is 7, so rightTriangleArea(3, 5) would return 7.0. Divide
by 2.0 (a double literal) instead: that forces the division into floating
point and keeps the .5. Return type is double, so (a * b) / 2.0 is what you
want.
Task 4 — calling std::sqrt
#include <cmath> is already at the top of the file. Then
return std::sqrt(a * a + b * b);. The argument a*a + b*b is an int
expression; std::sqrt takes a double, so the int is converted automatically
and you get a double back.
Task 5 — composition (functions calling functions)
You already wrote hypotenuse. The rectangle's diagonal is exactly the
hypotenuse of the triangle formed by its two sides, so just hand them over:
double rectangleDiagonal(int width, int height)
{
return hypotenuse(width, height); // no geo:: prefix needed inside namespace geo
}This is "Don't Repeat Yourself" (2.2): one definition of the Pythagoras math,
reused. If you instead pasted the std::sqrt formula again, the cross-check test
would still pass, but you'd be maintaining the same math in two places — exactly
what functions exist to prevent.
Stuck on a build error instead of a test failure?
- "
areawas not declared in this scope" — make sure your body is insidenamespace geo { ... }. Outside it, you'd be defining a different globalarea, andgeo::areawould still be undefined (a linker error). - "
geo.hfile not found" — build through the Makefile; it passes-I.so the simple-name include resolves. - A return-type or signature mismatch is caught at compile time precisely
because
geo.cppincludes its own header — fix the body to match the prototype ingeo.h(that's the safety the.cpp-includes-its-.hrule buys you).
Stretch goals
- Add
circleArea(int radius)andcircleCircumference(int radius)using ageo::piconstant — practice adding to a namespace and to the header (you can do this now withconst/constexprarriving fully in Ch 5). - Add
squareArea(int side)implemented by callingrectangleArea(side, side)— more composition. - Return both a shape's area and perimeter from one call (needs a
struct/class, Ch 13) — for now, notice why a function returns exactly one value (2.2). - Reject negative sides with a message (needs
if, Ch 4/8) — then validate inputs properly (Ch 9). - Split
geointo two headers (rect.h,triangle.h) and watch how header guards and include order (2.11, 2.12) keep everything from colliding.
// ============================================================================
// starter/geo.cpp — the IMPLEMENTATION of the geo library (STARTER)
// ----------------------------------------------------------------------------
// This is the file you edit. The header geo.h already DECLARED these five
// functions; here you DEFINE them (write the bodies). Each one sits inside
// `namespace geo { ... }` so its full name matches the declaration in the
// header — geo::rectangleArea, geo::hypotenuse, and so on.
//
// Fill in the five TASK blocks. Build / grade with:
// make build compile the starter (it already compiles)
// make test run the unit tests -> RED until you finish the tasks
// make solution / make test-solution the reference, for when you're stuck
//
// WHY each stub currently returns a wrong-but-compiling placeholder: a value-
// returning function must return a value on every path (2.2) — running off the
// end is undefined behavior. The placeholders keep the file compiling and
// warning-clean so your FIRST `make test` is a clean RED, not a build error.
// Replacing them with the real formulas turns that red into green.
// ============================================================================
#include "geo.h" // 2.11 best practice: a .cpp #includes its OWN paired header
// so the compiler checks your bodies against the contract.
#include <cmath> // std::sqrt — you will need it for the hypotenuse.
namespace geo
{
// ─── TASK 1: rectangleArea ─────────────────────────────────────────────
// Return the area of a width x height rectangle: width * height.
// Both parameters are int; the product is int. Easiest one — warm up.
//
// >>> YOUR CODE HERE <<<
int rectangleArea(int width, int height)
{
return width + height; // placeholder: wrong (should be *, not +),
// but compiles warning-clean. Replace it.
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 2: rectanglePerimeter ────────────────────────────────────────
// Return the perimeter (the distance around all four sides):
// 2 * width + 2 * height.
//
// >>> YOUR CODE HERE <<<
int rectanglePerimeter(int width, int height)
{
return width + height; // placeholder: wrong (forgets the other two
// sides), but compiles warning-clean. Replace it.
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 3: rightTriangleArea ─────────────────────────────────────────
// Return HALF the bounding rectangle's area: (a * b) / 2.
// Watch the types: a and b are int, but you must return a double, and for
// an ODD product (e.g. 3 * 5 = 15) the answer is 7.5, NOT 7. Make the
// division happen in floating point — for example divide by 2.0, so the
// fractional half is kept instead of being chopped off by integer division.
//
// >>> YOUR CODE HERE <<<
double rightTriangleArea(int a, int b)
{
return a + b; // placeholder
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 4: hypotenuse ────────────────────────────────────────────────
// Return the length of the long slanted side by the Pythagorean theorem:
// c = square_root(a*a + b*b).
// Use std::sqrt (from <cmath>, already included above). a*a + b*b is an int
// expression; std::sqrt takes/returns a double, so the result comes back as
// a double automatically.
//
// >>> YOUR CODE HERE <<<
double hypotenuse(int a, int b)
{
return a + b; // placeholder
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 5: rectangleDiagonal (the stretch — COMPOSE, don't re-derive) ─
// The diagonal of a width x height rectangle is the hypotenuse of the right
// triangle whose two legs ARE the rectangle's sides. So DON'T call std::sqrt
// again here — CALL the geo::hypotenuse function you just wrote, passing the
// width and height as its two legs, and return whatever it gives you. This
// is one of your own functions calling another (2.1 / 2.6). Because both
// live in `namespace geo`, you can call it as just hypotenuse(width, height)
// from in here (no geo:: prefix needed inside the namespace).
//
// >>> YOUR CODE HERE <<<
double rectangleDiagonal(int width, int height)
{
return width + height; // placeholder
}
// ───────────────────────────────────────────────────────────────────────
}
Try the lab first — the learning is in the attempt.
// ============================================================================
// solution/geo.cpp — reference IMPLEMENTATION of the geo library
// ----------------------------------------------------------------------------
// One correct, warning-clean way to write the five bodies. Peek only after you
// have tried your own — you learn the .h/.cpp split by getting YOUR version to
// pass `make test` first.
//
// Notice the shape: same #include of the paired header, same `namespace geo`
// wrapper, and the bodies match the prototypes in geo.h exactly. This Foo.h /
// Foo.cpp pairing is the template for every LLVM pass you will write later.
// ============================================================================
#include "geo.h" // include our own paired header (2.11) — compile-time check
#include <cmath> // std::sqrt
namespace geo
{
// TASK 1 — both ints, product is int.
int rectangleArea(int width, int height)
{
return width * height;
}
// TASK 2 — all four sides: two widths + two heights.
int rectanglePerimeter(int width, int height)
{
return 2 * width + 2 * height;
}
// TASK 3 — divide by 2.0 (a double literal) so the division is done in
// floating point and the ".5" on odd products survives. Writing `/ 2`
// instead would truncate (15/2 == 7) before the double conversion — a
// classic integer-division bug.
double rightTriangleArea(int a, int b)
{
return (a * b) / 2.0;
}
// TASK 4 — Pythagoras. a*a + b*b is an int; std::sqrt promotes it to double
// and returns a double.
double hypotenuse(int a, int b)
{
return std::sqrt(a * a + b * b);
}
// TASK 5 — COMPOSITION: the diagonal IS the hypotenuse of the triangle made
// by the rectangle's two sides. Reuse the function above instead of redoing
// the square-root math (DRY, 2.2 / 2.6). Inside namespace geo we can call it
// unqualified as hypotenuse(...).
double rectangleDiagonal(int width, int height)
{
return hypotenuse(width, height);
}
}
// ============================================================================
// tests/tests.cpp — the automated grader for the geo library (Chapter 2)
// ----------------------------------------------------------------------------
// This is a CONSUMER of your library: like a tiny main(), it #includes the
// header (the contract) and calls the API through the geo:: namespace. It does
// NOT include any .cpp — the Makefile compiles this file together with EITHER
// starter/geo.cpp (the `make test` target) OR solution/geo.cpp (the
// `make test-solution` target) and lets the LINKER join the call to its
// definition (2.8). That separation — declarations here, definition compiled
// in separately — is the whole point of the chapter.
//
// Tiny no-framework harness: CHECK fails loudly with the line number; the
// program's EXIT CODE (0 = all good, 1 = something failed) is what `make`
// turns into PASS / FAIL.
// ============================================================================
#include <iostream>
#include <cmath> // std::abs for the floating-point comparison
#include "geo.h" // the library's public interface (declarations only).
// Found via the -I. include path set in the Makefile.
static int fails = 0;
// Exact-equality check — for the int-returning functions, where == is correct.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
// Floating-point check — doubles can carry tiny rounding dust, so compare
// "close enough" rather than bit-for-bit. (You'll formalize float comparison in
// Chapter 6; a small fixed epsilon is plenty for these whole-number answers.)
#define CHECK_NEAR(got, want) \
do { double g_=(got), w_=(want); if(std::abs(g_ - w_) > 1e-9){ \
std::cerr << "FAIL: " #got " == " #want " (got " << g_ << ", want " << w_ \
<< ") @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── geo::rectangleArea ────────────────────────────────────────────────
CHECK(geo::rectangleArea(3, 4) == 12);
CHECK(geo::rectangleArea(7, 1) == 7);
CHECK(geo::rectangleArea(10, 10) == 100);
CHECK(geo::rectangleArea(0, 5) == 0); // edge case: zero-width → zero area
// ── geo::rectanglePerimeter ───────────────────────────────────────────
CHECK(geo::rectanglePerimeter(3, 4) == 14); // 2*3 + 2*4
CHECK(geo::rectanglePerimeter(5, 5) == 20); // square
CHECK(geo::rectanglePerimeter(0, 6) == 12); // edge case: degenerate side
// ── geo::rightTriangleArea ────────────────────────────────────────────
CHECK_NEAR(geo::rightTriangleArea(3, 4), 6.0); // even product → whole
CHECK_NEAR(geo::rightTriangleArea(6, 8), 24.0);
CHECK_NEAR(geo::rightTriangleArea(3, 5), 7.5); // EDGE: odd product must be
// 7.5, not 7 — this is the
// integer-division trap.
// ── geo::hypotenuse ───────────────────────────────────────────────────
// Pythagorean triples chosen so the answers are exact whole numbers.
CHECK_NEAR(geo::hypotenuse(3, 4), 5.0);
CHECK_NEAR(geo::hypotenuse(5, 12), 13.0);
CHECK_NEAR(geo::hypotenuse(6, 8), 10.0);
// ── geo::rectangleDiagonal (must REUSE geo::hypotenuse) ──────────────
CHECK_NEAR(geo::rectangleDiagonal(3, 4), 5.0); // same 3-4-5 triangle
CHECK_NEAR(geo::rectangleDiagonal(8, 6), 10.0); // legs swapped: still 10
// Cross-check the composition: the diagonal of a w x h rectangle MUST equal
// hypotenuse(w, h). If Task 5 re-derived the math wrong, this still catches
// it; if it correctly delegates, the two are equal by construction.
CHECK_NEAR(geo::rectangleDiagonal(5, 12), geo::hypotenuse(5, 12));
if (!fails) std::cout << "PASS ✅ all geo checks passed.\n";
else std::cout << "FAIL ❌ " << fails << " check(s) failed — see lines above.\n";
return fails ? 1 : 0;
}
// ============================================================================
// demo.cpp — a tiny program that USES the geo library (provided, complete)
// ----------------------------------------------------------------------------
// This is the "application" side. It knows NOTHING about how the geo functions
// work — it only #includes the header (the contract) and calls the API through
// the geo:: qualifier. The actual code that runs comes from whichever geo.cpp
// the Makefile links in (starter/ for `make run`, solution/ for `make solution`).
//
// You don't have to edit this file. Run it with `make run` to watch your
// library in action: while your bodies are still stubs the numbers are wrong,
// and they become correct as you finish each task. (`make test` is the real
// grader; this is just a friendly demonstration.)
// ============================================================================
#include <iostream>
#include "geo.h" // declarations only — the library's public interface
int main()
{
// A 3 x 4 rectangle is the running example (its diagonal closes a 3-4-5
// right triangle, so every number below is clean).
int w { 3 };
int h { 4 };
std::cout << "geo demo — rectangle " << w << " x " << h << "\n";
std::cout << " area: " << geo::rectangleArea(w, h) << "\n";
std::cout << " perimeter: " << geo::rectanglePerimeter(w, h) << "\n";
std::cout << " diagonal: " << geo::rectangleDiagonal(w, h) << "\n";
std::cout << "right triangle with legs " << w << " and " << h << "\n";
std::cout << " area: " << geo::rightTriangleArea(w, h) << "\n";
std::cout << " hypotenuse: " << geo::hypotenuse(w, h) << "\n";
return 0;
}
// ============================================================================
// geo.h — the PUBLIC INTERFACE of the geo mini-library (Chapter 2)
// ----------------------------------------------------------------------------
// This header is COMPLETE and PROVIDED. You do not edit it. Read it closely —
// a header is a contract. It tells every other file *what functions exist*,
// *what they take*, and *what they return*, WITHOUT revealing how they work.
// The bodies (the "how") live in a separate .cpp file that you implement.
//
// Three Chapter-2 ideas are on display in this one little file:
//
// 1) HEADER GUARD — the #ifndef / #define / #endif sandwich below stops the
// file's contents from being pasted in twice if it gets #included more
// than once in the same translation unit (2.12). The guard macro is the
// FILENAME IN ALL CAPS with punctuation turned into underscores: GEO_H.
//
// 2) NAMESPACE — every name lives inside `namespace geo { ... }` (2.9),
// so you call them as geo::rectangleArea(...). The qualifier keeps these
// names from colliding with anyone else's `area` or `perimeter`.
//
// 3) DECLARATIONS ONLY — a header holds forward declarations (prototypes):
// return type, name, parameter types, semicolon, NO body (2.7, 2.11).
// Putting a *definition* here would break the moment two .cpp files
// included it (an ODR / "duplicate definition" linker error).
//
// CS6340 / LLVM lens: every LLVM pass you will write looks exactly like this —
// a `Foo.h` that declares an API inside a namespace behind a header guard, and
// a `Foo.cpp` that defines the bodies. You are building that muscle now.
// ============================================================================
#ifndef GEO_H // if GEO_H has not been defined yet...
#define GEO_H // ...define it now, so a 2nd include skips the body
namespace geo // a named scope region: everything here is geo::name
{
// ── Rectangles (use int — whole-number sides) ──────────────────────────
//
// area = width * height. Returns the enclosed area.
int rectangleArea(int width, int height);
// perimeter = twice the width plus twice the height (all four sides).
int rectanglePerimeter(int width, int height);
// ── Right triangles (use int legs; the hypotenuse may be fractional) ───
//
// A right triangle has two perpendicular "legs" a and b meeting at the
// square corner. Its area is half of the bounding rectangle: (a * b) / 2.
// Returns a double because halving an odd product is not a whole number.
double rightTriangleArea(int a, int b);
// The hypotenuse is the long slanted side, by the Pythagorean theorem:
// c = square_root(a*a + b*b).
// Returns a double (square roots are rarely whole numbers).
double hypotenuse(int a, int b);
// ── Composition: a function built FROM the functions above ─────────────
//
// The straight-line diagonal of a width x height rectangle is the
// hypotenuse of the right triangle whose legs are the rectangle's sides.
// Implement this by REUSING geo::hypotenuse — do not re-derive the math.
// (This is the "functions call functions" idea made physical, 2.1 / 2.6.)
double rectangleDiagonal(int width, int height);
}
#endif // GEO_H
# Chapter 2 — geo mini-library · unit-test grader.
# Targets follow the drills/CLAUDE.md Makefile contract. (Recipes use TABS.)
#
# Multi-file build (the point of the chapter): the program/grader is compiled
# from TWO .cpp files at once — a consumer (demo.cpp or tests/tests.cpp) plus an
# implementation (starter/geo.cpp or solution/geo.cpp). geo.h is included by all
# of them; it is never compiled on its own.
CXX := clang++
# -I. puts the chapter root on the include path, so every file can write the
# paired-header include by its simple name: #include "geo.h" (matches the 2.11
# add.h/add.cpp convention, and how real projects — and LLVM — find headers).
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── Starter: link demo.cpp against the LEARNER's starter/geo.cpp ────────────
build: starter/app
starter/app: demo.cpp geo.h starter/geo.cpp
$(CXX) $(CXXFLAGS) demo.cpp starter/geo.cpp -o starter/app
run: build
./starter/app
# ── The grader: compile the tests with the LEARNER's starter/geo.cpp ───────
# RED until the TASK blocks are filled in; GREEN once they are.
test: tests/tests.cpp geo.h starter/geo.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp starter/geo.cpp -o tests/run
@./tests/run || echo "FAIL ❌ fill in the TASK blocks in starter/geo.cpp until every check passes."
# ── The reference solution, built + run as a program ───────────────────────
solution: solution/app
./solution/app
solution/app: demo.cpp geo.h solution/geo.cpp
$(CXX) $(CXXFLAGS) demo.cpp solution/geo.cpp -o solution/app
# ── Proof the exercise is solvable: tests + solution/geo.cpp MUST be green ──
test-solution: tests/tests.cpp geo.h solution/geo.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp solution/geo.cpp -o tests/run
@./tests/run
clean:
rm -f starter/app solution/app tests/run
make test locally
(see “Build & run locally” above).