Chapter 21 — Operator Overloading: Fraction v2
You built Fraction in Chapter 14: constructors, equals(), multipliedBy(),
print helpers. It worked, but it felt clunky — you had to write a.multipliedBy(b)
instead of a * b, and a.equals(b) instead of a == b.
Fraction v2 fixes that. You will add nine families of operator overloads so
that Fraction feels like a built-in numeric type:
Fraction a{1,2}, b{1,3};
Fraction c { a * b }; // operator*
Fraction d { a + b }; // operator+
bool same { a == b }; // operator==
bool diff { a != b }; // operator!=
bool less { b < a }; // operator<
Fraction neg{ -a }; // unary operator-
++a; // prefix operator++
a++; // postfix operator++
std::cout << a; // operator<<The Fraction class is interesting precisely because it has a class
invariant — the stored form is always fully reduced, and the denominator is
always positive. Every operator you write must preserve that invariant. That
is the real payoff: once the invariant holds, operator== is a trivial field
comparison with no cross-multiplication, and operator< only needs to
cross-multiply once.
Your tasks
Work through starter/fraction.cpp in order. Each TASK n block maps 1:1 to
one of the numbered tasks below.
Task 1 — operator*(Fraction, Fraction)
Fraction multiplication: (a/b) * (c/d) = (a*c)/(b*d). Construct and return
a Fraction{a*c, b*d} — the two-argument constructor calls reduce() for you.
Both operands are const Fraction&; return a new Fraction by value.
Task 2 — operator*(Fraction, int) and operator*(int, Fraction)
Treat the int as a whole-number fraction n/1. Implement Fraction * int
directly; make int * Fraction delegate to it (one-liner: return f * n;).
Task 3 — operator+(Fraction, Fraction)
Standard formula: (a/b) + (c/d) = (a*d + b*c) / (b*d). Again, pass through
the two-argument constructor so reduce() runs on the result.
Task 4 — operator== and operator!=
Because both fractions are fully reduced, equal fractions have identical stored
fields. operator== is a field comparison; operator!= is !(a == b).
Task 5 — operator<
Cross-multiplication: a/b < c/d iff a*d < b*c. Both denominators are always
positive (invariant), so multiplying both sides does not flip the inequality.
Keep test values small — the grader uses |num| ≤ 20, |den| ≤ 20.
Task 6 — unary operator-
Returns Fraction{-m_numerator, m_denominator}. Does not modify *this
(const member). One operand → member function.
Task 7 — prefix operator++
Add exactly 1: (num/den) + 1 = (num + den) / den. Update m_numerator
in-place, call reduce(), and return *this by reference.
Task 8 — postfix operator++
Three-step pattern: (1) copy *this into old; (2) call prefix ++(*this);
(3) return old by value. The dummy int parameter marks postfix — ignore it.
Task 9 — operator<<
Print "num/den" to the stream, then return the stream by reference. Must be
a non-member (left operand is std::ostream). Access m_numerator and
m_denominator through the friend declaration in fraction.h.
Success criteria
make buildcompilesstarter/fraction.cppwith zero warnings under-Wall -Wextra.make testexits GREEN — all checks pass.- You can explain why
postfix x++andprefix ++xreturn different things, and whyoperator<<cannot be a member ofFraction.
Concepts practiced
New (Chapter 21):
- Operator overloading — making user types feel built-in
- Friend non-member operators for symmetric binary operations (notes 21.2)
- Non-member
operator<<with stream-reference return (notes 21.4) - Member vs. friend decision (notes 21.1, 21.5)
- Unary operator- as const member (notes 21.6)
- Prefix
operator++: mutates and returns*thisby reference (notes 21.8) - Postfix
operator++: dummy-int signature, copy-then-increment, return old value (notes 21.8) - Comparison operators — reduced-form equality, cross-multiply ordering (notes 21.7)
- Implementing one operator in terms of another to minimize redundancy
Reused from earlier chapters:
- Class invariants, constructors, member-initializer lists (Chapter 14)
constmember functions, access specifiers,explicit(Chapter 14)- Friend declarations for private-data access (Chapter 15)
std::stringandstd::to_string(Chapter 5)
Constraints
Allowed:
- All C++ ≤ Chapter 21 concepts (constructors, const, friend, references, basic standard library)
- Calling
reduce()inside any operator that produces a newFraction - Delegating
operator!=tooperator==; delegatingint*FractiontoFraction*int - Delegating postfix
++to prefix++
Forbidden:
operator<=>(C++20 spaceship — explicitly out of scope for this course level)operator[]andoperator()(subscript/call operators — out of scope for this exercise)- Conversion operators (
operator int()etc.) — out of scope - Altering
fraction.h,tests/tests.cpp, orMakefile - Any solution that passes tests by hard-coding expected values
Required idioms:
operator!=must be expressed as!(lhs == rhs)— not a hand-rolled comparison- Postfix
++must save a copy before incrementing, then return that copy operator<<must returnstd::ostream&(for chaining)
Build & run locally
# Compile-check the starter (must succeed, zero warnings)
make build
# Run the grader against your starter code (RED until all tasks complete)
make test
# Peek at the reference solution output
make solution
# Verify the reference passes (must be GREEN — our proof the exercise is solvable)
make test-solution
# Remove build artifacts
make cleanHints
Hint 1 — Task 1 (operator* Fraction*Fraction)
The formula is (a*c)/(b*d). You have full access to both operands' private
fields because this is a friend function. Construct and return:
return Fraction { lhs.m_numerator * rhs.m_numerator,
lhs.m_denominator * rhs.m_denominator };The two-argument constructor calls reduce(), so the result is already in
lowest terms.
Hint 2 — Task 3 (operator+ addition formula)
To add fractions with different denominators, find a common denominator first.
The simplest (not necessarily the lowest) common denominator is b*d:
(a/b) + (c/d) = (a*d)/(b*d) + (b*c)/(b*d) = (a*d + b*c) / (b*d)
Pass those two expressions to Fraction{...} and reduce() does the rest.
Hint 3 — Task 4 (operator== reduced-form insight)
Because reduce() is always called in the two-argument constructor, two
mathematically equal fractions will have the same stored fields. So:
return (lhs.m_numerator == rhs.m_numerator)
&& (lhs.m_denominator == rhs.m_denominator);Then operator!= is a one-liner: return !(lhs == rhs);
Hint 4 — Task 5 (operator< cross-multiplication)
Cross-multiply to avoid dividing (which would need double):
a/b < c/d iff a*d < b*c (valid when b > 0 and d > 0)
The class invariant guarantees both denominators are positive, so the comparison direction is preserved. With the small test values used, there is no overflow risk.
Hint 5 — Task 7 vs Task 8 (prefix vs postfix ++)
Prefix (++x): increment then return the new value.
Fraction& Fraction::operator++()
{
m_numerator += m_denominator;
reduce();
return *this; // ref to the updated object
}Postfix (x++): save old, increment, return old.
Fraction Fraction::operator++(int) // the `int` is just a syntax marker
{
Fraction old { *this }; // copy before any change
++(*this); // call prefix++ (reuse!)
return old; // return the original value (by value, not ref)
}The test checks: Fraction old { f++ }; — old must equal the pre-increment
value, while f itself must have advanced by 1.
Hint 6 — Task 9 (operator<< non-member requirement)
std::cout << f desugars to operator<<(std::cout, f). The left operand is
std::ostream, so the function cannot be a member of Fraction (that would
require Fraction::operator<<(std::ostream&), which would need to be called
as f << std::cout — backwards).
The friend declaration in fraction.h already grants access to private
members. The body is two lines:
out << f.m_numerator << '/' << f.m_denominator;
return out;Stretch goals
These extend beyond the required tasks — use concepts from later chapters if you want to get ahead, and note which chapter each feature belongs to.
-
operator-(binary subtraction):a - b = a + (-b). Delegate tooperator-(unary) andoperator+. -
operator+=: a compound-assignment variant. Should modify*thisand return*this&. Then rewriteoperator+in terms ofoperator+=using a copy (the idiomatic implementation order). -
operator>,operator<=,operator>=: derive all three fromoperator<using the same minimize-redundancy principle. -
operator>>(stream extraction, Chapter 28 I/O): read a fraction from"num/den"input, validate that denominator is non-zero, setfailbiton bad input. (Formally a Chapter 28 concept — notes 21.4 mentions it briefly.) -
std::sortcompatibility: sinceoperator<is defined, astd::vector<Fraction>can be sorted withstd::sort(Chapter 18 algorithms). Try it.
// Chapter 21 — Operator Overloading · Fraction v2 (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// You built Fraction in Chapter 14. Now you upgrade it to speak C++ arithmetic
// natively. Each TASK below asks you to implement one family of operator
// overloads. Fill in the marked regions; everything else is scaffolding.
//
// The Makefile compiles THIS file together with tests/tests.cpp to produce the
// grader. The grader runs the same operator expressions a user would write —
// if your implementation is correct, it goes GREEN.
//
// ── WHAT'S ALREADY HERE ──────────────────────────────────────────────────────
//
// fraction.h — the complete class declaration, with m_numerator,
// m_denominator, constructors, accessors, and the private
// gcd()/reduce() helpers. Read it carefully before editing here.
//
// ── OPERATOR DESIGN RULES (notes 21.1, 21.5, 21.6, 21.8) ────────────────────
//
// Operator | Return type | Why
// ──────────────┼────────────────────┼───────────────────────────────────────
// binary arith | new Fraction | does not modify either operand
// comparison | bool | observes only; never mutates
// unary - | new Fraction | does not modify *this
// prefix ++ | Fraction& | mutates *this, returns ref for chaining
// postfix ++ | Fraction (by val) | returns the OLD value (copy made first)
// operator<< | std::ostream& | chain: cout << a << b
//
// ── THE DUMMY-INT TRICK (notes 21.8) ─────────────────────────────────────────
//
// C++ has only one operator++ name. To tell prefix from postfix, the language
// uses a "dummy int" parameter in the POSTFIX overload:
//
// Fraction& operator++(); // prefix: ++x -> Fraction::operator++()
// Fraction operator++(int); // postfix: x++ -> Fraction::operator++(0)
//
// When you write x++, the compiler supplies 0 for that argument (see the
// rewrite above), and the value carries no meaning — it is purely a syntax
// marker. Do not use the dummy parameter in your body.
//
// ── WARNING-CLEAN PLACEHOLDERS ───────────────────────────────────────────────
// Each stub uses (void) casts to silence -Wunused-parameter warnings so the
// starter compiles warning-clean out of the box. Remove the (void) casts when
// you write the real body — you'll need the parameter names.
#include "../fraction.h"
// ─── TASK 1: operator*(Fraction, Fraction) ───────────────────────────────────
// Fraction multiplication: (a/b) * (c/d) = (a*c) / (b*d).
//
// Construct and RETURN a new Fraction — use the two-argument constructor
// Fraction{num, den} which calls reduce() automatically. Return by value;
// C++17 copy elision means no copy overhead (notes 14.15).
//
// friend non-member: both operands are Fraction, no "left" object is privileged.
// Access rhs.m_numerator and rhs.m_denominator directly (friend private access).
//
// Constraints: const refs in, new Fraction out. No mutation of lhs or rhs.
//
Fraction operator*(const Fraction& lhs, const Fraction& rhs)
{
// ─── TASK 1: operator*(Fraction, Fraction) ───────────────────────────────
// (a/b) * (c/d) = (a*c)/(b*d). Use the two-arg constructor so reduce() runs.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)lhs; (void)rhs; // suppress unused warnings from placeholder
return Fraction{ 0 }; // placeholder — remove and replace with the real body
}
// ─── TASK 2: operator*(Fraction, int) and operator*(int, Fraction) ────────────
// A Fraction times a whole number n is the same as Fraction * Fraction{n,1}.
// Implement operator*(Fraction, int) directly; make operator*(int, Fraction)
// delegate to it so you write the arithmetic only once (notes 21.2 — reuse).
//
Fraction operator*(const Fraction& f, int n)
{
// ─── TASK 2a: operator*(Fraction, int) ───────────────────────────────────
// Treat n as a whole-number fraction n/1. Return a new Fraction.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)f; (void)n; // suppress unused warnings from placeholder
return Fraction{ 0 }; // placeholder
}
Fraction operator*(int n, const Fraction& f)
{
// ─── TASK 2b: operator*(int, Fraction) ───────────────────────────────────
// Reuse operator*(Fraction, int) — swap operand order, delegate.
// One-liner: return f * n;
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)n; (void)f; // suppress unused warnings from placeholder
return Fraction{ 0 }; // placeholder
}
// ─── TASK 3: operator+(Fraction, Fraction) ───────────────────────────────────
// Fraction addition: (a/b) + (c/d) = (a*d + b*c) / (b*d).
//
// Again, return a new Fraction{...} so reduce() runs and the result is in
// lowest terms. Does NOT modify lhs or rhs.
//
Fraction operator+(const Fraction& lhs, const Fraction& rhs)
{
// ─── TASK 3: operator+(Fraction, Fraction) ───────────────────────────────
// (a/b) + (c/d) = (a*d + b*c) / (b*d), then reduce.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)lhs; (void)rhs; // suppress unused warnings from placeholder
return Fraction{ 0 }; // placeholder
}
// ─── TASK 4: operator== and operator!= ───────────────────────────────────────
// Because both fractions are fully reduced (see invariant in fraction.h), two
// equal fractions have IDENTICAL m_numerator AND m_denominator.
// No cross-multiplication needed — just compare the two fields.
//
// operator!= should express itself in terms of operator==: !(a == b).
// (notes 21.7 — minimize redundancy, keep definitions consistent)
//
bool operator==(const Fraction& lhs, const Fraction& rhs)
{
// ─── TASK 4a: operator== ─────────────────────────────────────────────────
// Both fractions are reduced; equal fractions have identical stored fields.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)lhs; (void)rhs; // suppress unused warnings from placeholder
return false; // placeholder — always returns false (tests will fail)
}
bool operator!=(const Fraction& lhs, const Fraction& rhs)
{
// ─── TASK 4b: operator!= — express this as !(lhs == rhs) ─────────────────
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)lhs; (void)rhs; // suppress unused warnings from placeholder
return true; // placeholder
}
// ─── TASK 5: operator< ───────────────────────────────────────────────────────
// a/b < c/d iff a*d < b*c (cross-multiplication, denominators always > 0).
//
// Because m_denominator is ALWAYS positive (the invariant), multiplying both
// sides of a/b < c/d by (b*d) — which is positive — does NOT flip the
// inequality direction.
//
// OVERFLOW NOTE (engineering aside — not in the chapter notes): the tests use
// |num| ≤ 20 and |den| ≤ 20, so the worst case is 20 * 20 = 400, well within
// int range. For a production library you would use long long or a checked
// multiply. (The ordering rule itself is notes 21.7; overflow is our own note.)
//
bool operator<(const Fraction& lhs, const Fraction& rhs)
{
// ─── TASK 5: operator< via cross-multiplication ───────────────────────────
// a/b < c/d iff a*d < b*c (both denominators positive -> safe to cross-
// multiply without flipping the comparison direction).
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)lhs; (void)rhs; // suppress unused warnings from placeholder
return false; // placeholder
}
// ─── TASK 6: unary operator- ─────────────────────────────────────────────────
// Returns a NEW Fraction with the numerator sign flipped; denominator unchanged.
// *this is NOT modified (const member function).
//
// One operand -> member function (notes 21.6).
// Return by value (the new value, not a reference to a local).
//
Fraction Fraction::operator-() const
{
// ─── TASK 6: unary operator- (negation) ──────────────────────────────────
// Return a new Fraction with numerator negated: Fraction{-m_numerator, m_denominator}.
// The denominator is already positive, so no reduce() needed — but passing
// through the two-arg constructor is fine and harmless.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
return Fraction{ 0 }; // placeholder — wrong sign, tests will catch it
}
// ─── TASK 7: prefix operator++ ────────────────────────────────────────────────
// Add exactly 1 to the fraction. 1 as a fraction = denominator/denominator,
// so: (num/den) + 1 = (num + den) / den.
//
// Mutate *this, then return *this by reference (notes 21.8):
// m_numerator += m_denominator;
// reduce();
// return *this;
//
// Returning by reference lets chaining work: ++(++x). (Unusual in practice,
// but the return convention MUST match built-in prefix++ behavior.)
//
Fraction& Fraction::operator++()
{
// ─── TASK 7: prefix operator++ ────────────────────────────────────────────
// (num/den) + 1 = (num + den) / den. Modify m_numerator, call reduce(),
// return *this by reference.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
return *this; // placeholder — does nothing (no increment)
}
// ─── TASK 8: postfix operator++ ───────────────────────────────────────────────
// Return the OLD value THEN increment. The dummy int parameter (always 0) is
// the C++ syntax marker for postfix — do not use its value.
//
// Pattern (notes 21.8):
// 1. Copy *this into a local named `old`.
// 2. Increment *this (call the prefix ++ you just wrote).
// 3. Return `old` by value.
//
// Why by value? The local `old` is destroyed when this function returns, so
// returning a reference to it would be dangling.
//
Fraction Fraction::operator++(int /*unused*/)
{
// ─── TASK 8: postfix operator++ ────────────────────────────────────────────
// Save *this (the old value), then call prefix ++(*this), then return old.
// The dummy `int` parameter is 0 — ignore it.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
return Fraction{ 0 }; // placeholder — returns wrong value
}
// ─── TASK 9: operator<< (stream insertion) ────────────────────────────────────
// Output format: "num/den" e.g. Fraction{3,4} prints "3/4"
// e.g. Fraction{-1,2} prints "-1/2"
// e.g. Fraction{0} prints "0/1"
//
// Return the stream BY REFERENCE so chaining works:
// std::cout << a << " and " << b << '\n';
// Each link in that chain receives and returns the same stream object.
//
// MUST be non-member: the left operand is std::ostream (not Fraction), so there
// is no *this to put this in as a member. It is declared `friend` in
// fraction.h so we can access m_numerator / m_denominator directly.
//
// (The tests feed output into std::ostringstream — a preview of Ch 28's
// in-memory string streams. The grader infrastructure is provided as
// scaffolding; you do not need to understand ostringstream to complete the task.)
//
std::ostream& operator<<(std::ostream& out, const Fraction& f)
{
// ─── TASK 9: operator<< ───────────────────────────────────────────────────
// Write f.m_numerator, then '/', then f.m_denominator to `out`. Return `out`.
//
// >>> YOUR CODE HERE <<<
//
// ────────────────────────────────────────────────────────────────────────
(void)f; // suppress unused warnings from placeholder
out << "?/?"; // placeholder — wrong output, tests will catch it
return out;
}
Try the lab first — the learning is in the attempt.
// Chapter 21 — Operator Overloading · Fraction v2 (REFERENCE SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// Complete, correct, warning-clean implementation of ../fraction.h.
// Peek only AFTER a real attempt at starter/fraction.cpp — the learning is in
// reasoning about operator return types and member-vs-friend choices yourself;
// then compare here.
//
// IMPORTANT CONCEPTS TO OBSERVE in this file:
//
// 1. FRIEND BINARY OPS (tasks 1–5): both operands are Fraction, so neither is
// "the object." We implement them as non-member friend functions that access
// m_numerator and m_denominator directly — no public getters needed.
//
// 2. MEMBER UNARY OP (task 6): only one operand (*this). Member is the natural
// choice; returns a new value, so const and returns by value.
//
// 3. PREFIX vs POSTFIX ++ (tasks 7/8): the dummy-int trick. Prefix mutates and
// returns *this by reference. Postfix saves a copy FIRST, increments second,
// returns the copy — so the caller sees the OLD value. This is the #1 gotcha.
//
// 4. OPERATOR<< as FRIEND NON-MEMBER (task 9): left operand is std::ostream,
// not Fraction — so it can never be a member of Fraction. Returns stream
// by reference for chain-ability.
//
// 5. IMPLEMENT OP IN TERMS OF OP: operator!= delegates to operator==;
// operator*(int,Fraction) delegates to operator*(Fraction,int);
// postfix++ delegates to prefix++. This keeps logic in ONE place.
//
// CS6340 lens: when you read LLVM source, every ++it, it != end, *it,
// out << value you see is exactly these patterns on iterators, streams, and
// APInt. Translating it != end to operator!=(it, end) to !(it == end)
// makes the code no longer magical.
#include "../fraction.h"
// ─── TASK 1: operator*(Fraction, Fraction) ───────────────────────────────────
// (a/b) * (c/d) = (a*c)/(b*d).
//
// We construct Fraction{a*c, b*d} and rely on the two-arg constructor's
// call to reduce() to put the result in lowest terms automatically.
// Same-class private access (friend): we read m_numerator/m_denominator
// directly, avoiding the need for public getters in the hot path.
//
// Notes 21.2: "arithmetic operators usually do not modify either operand."
// Both lhs and rhs are const-ref; return is by value.
Fraction operator*(const Fraction& lhs, const Fraction& rhs)
{
return Fraction { lhs.m_numerator * rhs.m_numerator,
lhs.m_denominator * rhs.m_denominator };
}
// ─── TASK 2: operator*(Fraction, int) and operator*(int, Fraction) ────────────
// A whole number n is the fraction n/1. Rather than duplicating the arithmetic,
// operator*(int, Fraction) simply delegates to operator*(Fraction, int) — the
// "implement in terms of a smaller set" principle (notes 21.2).
//
// operator*(Fraction, int): multiply numerator by n, denominator unchanged.
Fraction operator*(const Fraction& f, int n)
{
// n as a fraction is n/1, so (a/b) * (n/1) = (a*n) / (b*1).
return Fraction { f.m_numerator * n, f.m_denominator };
}
// operator*(int, Fraction): reverse operands and call the Fraction*int overload.
// No arithmetic here — pure delegation. This is the standard pattern for
// symmetric mixed-type operators (notes 21.2).
Fraction operator*(int n, const Fraction& f)
{
return f * n; // delegate: calls operator*(const Fraction&, int) above
}
// ─── TASK 3: operator+(Fraction, Fraction) ───────────────────────────────────
// Standard fraction addition formula:
// (a/b) + (c/d) = (a*d + b*c) / (b*d)
//
// The Fraction{...} constructor calls reduce(), so the result is automatically
// in lowest terms (e.g. 1/4 + 1/4 = 2/8 -> stored as 1/4).
//
// Pitfall: do NOT forget to reduce. Without it, 1/4 + 1/4 would store as 2/8
// and operator== would incorrectly claim 2/8 != 1/4.
Fraction operator+(const Fraction& lhs, const Fraction& rhs)
{
return Fraction { lhs.m_numerator * rhs.m_denominator
+ rhs.m_numerator * lhs.m_denominator,
lhs.m_denominator * rhs.m_denominator };
}
// ─── TASK 4: operator== and operator!= ───────────────────────────────────────
// INSIGHT: because reduce() is called on construction, equal fractions have
// IDENTICAL m_numerator and m_denominator. There is no need for
// cross-multiplication. This is the payoff for maintaining the invariant.
//
// Example: Fraction{2,4} and Fraction{1,2} both store {1,2} after reduce(),
// so == is just (1==1) && (2==2) -> true. ✅
//
// operator!= is expressed as !(a == b) so there is ONE truth — any change
// to the equality definition is automatically inherited by inequality.
// (notes 21.7: "minimize redundancy")
bool operator==(const Fraction& lhs, const Fraction& rhs)
{
return (lhs.m_numerator == rhs.m_numerator)
&& (lhs.m_denominator == rhs.m_denominator);
}
bool operator!=(const Fraction& lhs, const Fraction& rhs)
{
return !(lhs == rhs); // delegate — do not duplicate the equality logic
}
// ─── TASK 5: operator< ───────────────────────────────────────────────────────
// Cross-multiplication: a/b < c/d iff a*d < b*c.
//
// WHY IS THIS SAFE? m_denominator is ALWAYS positive (the class invariant), so
// multiplying both sides of a/b < c/d by (b*d) — which is positive — does NOT
// flip the inequality direction.
//
// OVERFLOW NOTE (engineering aside — not in the chapter notes): the tests keep
// |num| ≤ 20 and |den| ≤ 20, so the worst case is 20 * 20 = 400, well within
// int range. For a production library you would use long long or a checked
// multiply. (The ordering rule itself is notes 21.7; overflow is our own note.)
bool operator<(const Fraction& lhs, const Fraction& rhs)
{
// a/b < c/d iff a*d < b*c (denominators positive -> safe to cross-mult)
return lhs.m_numerator * rhs.m_denominator
< rhs.m_numerator * lhs.m_denominator;
}
// ─── TASK 6: unary operator- ─────────────────────────────────────────────────
// Negation: -(a/b) = (-a)/b.
// The denominator is already positive, so we can pass it straight through the
// two-arg constructor (which calls reduce() — harmless because gcd(a,b) ==
// gcd(-a,b)). Returning by value creates a new Fraction.
//
// `const` member: does NOT modify *this. (notes 21.6)
Fraction Fraction::operator-() const
{
return Fraction { -m_numerator, m_denominator };
}
// ─── TASK 7: prefix operator++ ────────────────────────────────────────────────
// Add 1 to the fraction: (num/den) + 1 = (num + den) / den.
//
// We update m_numerator in-place, then call reduce() (the helper is accessible
// because we're inside a member function). Return *this by reference so that
// chaining (e.g. ++(++x)) would work — matching built-in prefix++ behavior.
//
// Not const: this function MUTATES the object (notes 21.5 const correctness).
Fraction& Fraction::operator++()
{
m_numerator += m_denominator; // add one whole unit (denominator/denominator)
reduce(); // re-normalize; handles sign and GCD
return *this; // return ref to the now-modified object
}
// ─── TASK 8: postfix operator++ ───────────────────────────────────────────────
// The classic three-step pattern (notes 21.8):
// 1. Save *this (the pre-increment value) into a local copy.
// 2. Increment *this (delegate to prefix++ — ONE source of truth).
// 3. Return the saved copy by VALUE.
//
// Returning by VALUE is mandatory: `old` is a local variable that will be
// destroyed when this function returns. A reference to it would dangle.
//
// The dummy `int` parameter is always 0; we name it `/*unused*/` to suppress
// the -Wunused-parameter warning that -Wextra would otherwise emit.
Fraction Fraction::operator++(int /*unused*/)
{
Fraction old { *this }; // step 1: copy the current (pre-increment) value
++(*this); // step 2: apply prefix++ (defined above)
return old; // step 3: return the OLD value (not a reference)
}
// ─── TASK 9: operator<< ───────────────────────────────────────────────────────
// Output "num/den" to the stream. Return stream by reference for chaining.
//
// MUST be a non-member: the expression cout << f maps to operator<<(cout, f).
// If this were a member of Fraction, the call shape would need to be f << cout
// — backwards and impossible to use naturally. (notes 21.4)
//
// `friend` in the header grants private access so we can read m_numerator and
// m_denominator without going through public getters.
//
// The tests pass an std::ostringstream (a string-backed stream) instead of
// std::cout so output can be checked as a string value. This is a grader
// infrastructure borrow — std::ostringstream is formally Chapter 28 I/O. It is
// provided as scaffolding; you do not need to understand it to complete the task.
std::ostream& operator<<(std::ostream& out, const Fraction& f)
{
out << f.m_numerator << '/' << f.m_denominator;
return out; // return the SAME stream for chaining
}
// Chapter 21 — Operator Overloading · Fraction v2 (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// Tiny no-framework unit-test harness (same style as drills/CLAUDE.md spec).
// Each CHECK that fails prints the expression and line number.
// Any failure -> non-zero exit -> `make test` is RED.
//
// The Makefile links this file against:
// starter/fraction.cpp (for `make test` — the learner's code)
// solution/fraction.cpp (for `make test-solution` — the reference)
//
// ── MACRO GOTCHA ─────────────────────────────────────────────────────────────
// The C preprocessor splits macro arguments on commas. Fraction{1,2} has a bare
// comma inside {}, which confuses the preprocessor into thinking it sees TWO
// arguments to CHECK(). Fix: assign the Fraction to a named variable first, or
// use the F() helper defined below (wraps Fraction{n,d} so the comma is inside a
// function call, not a raw braced-init — C++ parses it correctly before the
// preprocessor splits, because () is balanced before the macro expander runs).
//
// What makes tests MEANINGFUL here (rather than trivially true):
// 1. Values that catch "forgot to reduce": 1/4 + 1/4 should be 1/2 not 2/8.
// 2. Postfix++ must return the OLD value; prefix++ returns the NEW value.
// 3. operator<< verified by capturing into ostringstream and comparing strings.
// 4. operator*(int,Fraction) and operator*(Fraction,int) must give equal results.
// 5. Small values so cross-multiply in operator< stays well within int range.
//
// std::ostringstream: a string-backed stream from <sstream>.
// oss << value; writes into an internal buffer
// oss.str() returns the accumulated std::string
// This is a PREVIEW — formally Chapter 28 (I/O). Provided as grader scaffolding.
// ─────────────────────────────────────────────────────────────────────────────
#include <iostream>
#include <sstream> // std::ostringstream — grader infrastructure (preview Ch 28)
#include <string>
#include "../fraction.h"
static int fails = 0;
// CHECK: if condition is false, print the expression and line number; count it.
#define CHECK(cond) \
do { if(!(cond)){ \
std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; \
++fails; \
} } while(0)
// F(n,d) — convenience alias that lets you write F(1,2) inside CHECK()
// without triggering the preprocessor comma-split. F(n,d) is a function call;
// the C++ parser balances the () before the macro expander splits on commas.
static Fraction F(int n, int d) { return Fraction { n, d }; }
// Helper: stream a Fraction into an ostringstream and return the result string.
// Lets us test operator<< with a simple string comparison.
static std::string str(const Fraction& f)
{
std::ostringstream oss; // (grader plumbing — formally Ch 28 std::stringstream)
oss << f;
return oss.str();
}
int main()
{
// ── Task 1: operator*(Fraction, Fraction) ─────────────────────────────────
// Note: F(a,b) is the same as Fraction{a,b} — just safe inside CHECK().
{
Fraction half { 1, 2 };
Fraction third { 1, 3 };
CHECK((half * third) == F(1,6)); // (1/2)*(1/3) = 1/6
CHECK((F(2,3) * F(3,4)) == F(1,2)); // 6/12 -> 1/2
CHECK((F(3,4) * F(4,3)) == F(1,1)); // 12/12 -> 1/1
CHECK((F(0,1) * F(5,7)) == F(0,1)); // 0 * anything = 0
// Negative operands
CHECK((F(-1,2) * F(1,3)) == F(-1,6));
CHECK((F(-1,2) * F(-1,3)) == F(1,6)); // neg*neg = pos
// Result should be fully reduced
CHECK((F(2,4) * F(1,1)) == F(1,2)); // 2/4*1/1 = 2/4 -> 1/2
}
// ── Task 2: operator*(Fraction, int) and operator*(int, Fraction) ──────────
{
Fraction half { 1, 2 };
CHECK((half * 4) == F(2,1)); // (1/2)*4 = 4/2 -> 2/1
CHECK((4 * half) == F(2,1)); // 4*(1/2) = same
// Commutativity of mixed multiply
CHECK((half * 3) == (3 * half));
CHECK((F(2,3) * 3) == F(2,1)); // 6/3 -> 2/1
CHECK((F(1,5) * 0) == F(0,1)); // anything * 0 = 0
CHECK((0 * F(7,3)) == F(0,1));
// Negative int
CHECK((F(1,3) * -6) == F(-2,1));
}
// ── Task 3: operator+(Fraction, Fraction) ─────────────────────────────────
{
// Basic sums — result must be fully reduced
CHECK((F(1,4) + F(1,4)) == F(1,2)); // 2/8 -> 1/2
CHECK((F(1,3) + F(1,3)) == F(2,3));
CHECK((F(1,2) + F(1,3)) == F(5,6)); // 3/6+2/6=5/6
CHECK((F(1,6) + F(1,3)) == F(1,2)); // 1/6+2/6=3/6->1/2
// Adding zero
CHECK((F(3,4) + F(0,1)) == F(3,4));
// Negative addend
CHECK((F(3,4) + F(-1,4)) == F(1,2)); // 3/4-1/4=2/4->1/2
// Sum that reduces to 1
CHECK((F(1,4) + F(3,4)) == F(1,1)); // 4/4 -> 1/1
}
// ── Task 4: operator== and operator!= ─────────────────────────────────────
{
// Fractions that compare equal despite different construction
CHECK(F(1,2) == F(2,4)); // 2/4 reduces to 1/2
CHECK(F(2,3) == F(4,6)); // 4/6 reduces to 2/3
CHECK(F(0,1) == F(0,5)); // 0/5 reduces to 0/1
// Distinct fractions
CHECK(!(F(1,2) == F(1,3)));
CHECK(F(1,2) != F(1,3));
CHECK(!(F(3,4) != F(3,4))); // same fraction: != must be false
// Negative fractions (sign normalization)
CHECK(F(-1,2) == F(1,-2)); // 1/(-2) reduces to -1/2
CHECK(F(-2,4) == F(-1,2)); // both reduce to -1/2
}
// ── Task 5: operator< ──────────────────────────────────────────────────────
// All values have |num| <= 20, |den| <= 20 to stay safely within int.
{
CHECK(F(1,3) < F(1,2)); // 1/3 < 1/2
CHECK(F(1,4) < F(1,3)); // 1/4 < 1/3
CHECK(!(F(1,2) < F(1,3))); // 1/2 > 1/3
CHECK(!(F(1,2) < F(1,2))); // equal: NOT less-than
// Negative fractions
CHECK(F(-1,2) < F(1,2)); // negative < positive
CHECK(F(-3,4) < F(-1,4)); // -3/4 < -1/4
// Zero comparisons
CHECK(F(-1,2) < F(0,1)); // negative < zero
CHECK(F(0,1) < F(1,2)); // zero < positive
CHECK(!(F(0,1) < F(0,1))); // zero not less than zero
// Mixed denominators (the whole point of cross-mult)
CHECK(F(2,5) < F(3,7)); // 0.4 < ~0.43 -> true
CHECK(!(F(3,7) < F(2,5)));
}
// ── Task 6: unary operator- ────────────────────────────────────────────────
{
Fraction half { 1, 2 };
Fraction neg { -1, 2 };
CHECK(-half == neg); // -(1/2) = -1/2
CHECK(-neg == half); // -(-1/2) = 1/2
CHECK(-F(0,1) == F(0,1)); // -(0/1) = 0/1 (sign of 0)
// Original is unmodified
Fraction a { 3, 4 };
Fraction b { -a };
CHECK(a == F(3,4)); // a unchanged
CHECK(b == F(-3,4)); // b is the negation
}
// ── Task 7: prefix operator++ ─────────────────────────────────────────────
{
Fraction f { 1, 3 };
// Prefix ++ returns a reference to the updated object (notes 21.8).
Fraction& ref { ++f };
CHECK(f == F(4,3)); // 1/3 + 1 = (1+3)/3 = 4/3
CHECK(&ref == &f); // ref IS f (same address)
// Further increment
++f;
CHECK(f == F(7,3)); // 4/3 + 1 = (4+3)/3 = 7/3
// Increments that stay unreduced
Fraction g { 1, 2 };
++g; // (1+2)/2 = 3/2
CHECK(g == F(3,2));
// Increment from negative
Fraction h { -3, 4 };
++h; // (-3+4)/4 = 1/4
CHECK(h == F(1,4));
}
// ── Task 8: postfix operator++ ────────────────────────────────────────────
// THE KEY INVARIANT: postfix returns the value BEFORE the increment.
{
Fraction f { 1, 3 };
// postfix: old gets the PRE-increment value; f itself is incremented
Fraction old { f++ };
CHECK(old == F(1,3)); // old holds what f was BEFORE
CHECK(f == F(4,3)); // f itself was incremented
// Contrast prefix vs postfix explicitly
Fraction a { 2, 5 };
Fraction b { 2, 5 };
Fraction prefix_result { ++a }; // prefix: returns the new value
Fraction postfix_result { b++ }; // postfix: returns the old value
CHECK(prefix_result == F(7,5)); // prefix returned the updated a
CHECK(postfix_result == F(2,5)); // postfix returned the old b
CHECK(a == F(7,5)); // a was incremented
CHECK(b == F(7,5)); // b was also incremented
// After double postfix++: each call returned its own old value
Fraction c { 1, 5 };
c++; c++;
CHECK(c == F(11,5)); // 1/5 +1 -> 6/5 +1 -> 11/5
}
// ── Task 9: operator<< ────────────────────────────────────────────────────
{
CHECK(str(Fraction{ 1, 2 }) == "1/2");
CHECK(str(Fraction{ 3, 4 }) == "3/4");
CHECK(str(Fraction{-1, 2 }) == "-1/2");
CHECK(str(Fraction{ 0, 1 }) == "0/1");
CHECK(str(Fraction{ 1, 1 }) == "1/1");
// Auto-reduce: operator<< must print the REDUCED form
CHECK(str(F(2,4)) == "1/2"); // 2/4 -> 1/2 -> "1/2"
CHECK(str(F(6,4)) == "3/2"); // 6/4 -> 3/2 -> "3/2"
// Chain: two fractions in one stream expression
std::ostringstream oss; // (grader plumbing — formally Ch 28)
oss << F(1,2) << "+" << F(1,3);
CHECK(oss.str() == "1/2+1/3");
}
// ── Integration: operators compose naturally ───────────────────────────────
{
Fraction a { 1, 2 };
Fraction b { 1, 3 };
// -(a * b) == (-a) * b (negation distributes over multiplication)
CHECK(-(a * b) == (-a) * b);
// Adding a positive makes the sum larger
Fraction sum { a + b }; // 5/6
CHECK(b < sum); // 1/3 < 5/6 (use operator<)
// operator< round-trip
CHECK(b < a); // 1/3 < 1/2
CHECK(!(a < b));
CHECK(!(a < a));
// Postfix++ old value is strictly less than the new value
Fraction c { 1, 4 };
Fraction before { c++ };
CHECK(before < c); // old < new
}
// ── Final report ──────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all Fraction operator checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails
<< " check(s) failed — fix the TASK blocks in starter/fraction.cpp.\n";
return fails ? 1 : 0;
}
// ============================================================================
// fraction.h — Fraction v2: operator-overloaded rational numbers (Ch 21)
// ----------------------------------------------------------------------------
// This header is the CONTRACT between you and the grader. It DECLARES every
// operator the learner must implement. DO NOT EDIT THIS FILE — tests/tests.cpp
// and both starter/ and solution/ include it. Change a signature and nothing
// links.
//
// YOU BUILT THE FIRST VERSION in Chapter 14. That Fraction had:
// • Default and two-argument constructors (with reduce())
// • Const accessors: numerator(), denominator(), isValid()
// • Member functions: equals(), multipliedBy()
// • Print helpers: print(), toString()
//
// THIS VERSION, Fraction v2, keeps all of that and REPLACES the named helpers
// with OPERATOR OVERLOADS, so Fraction feels like a built-in numeric type:
//
// Fraction a{1,2}, b{1,3};
// Fraction c { a * b }; // operator*(Fraction, Fraction)
// Fraction d { a + b }; // operator+(Fraction, Fraction)
// bool same { a == b }; // operator==
// bool diff { a != b }; // operator!=
// bool less { b < a }; // operator<
// Fraction neg { -a }; // unary operator-
// ++a; // prefix operator++
// a++; // postfix operator++
// std::cout << a; // operator<<
//
// ── MEMBER vs FRIEND design (notes 21.1, 21.2, 21.4, 21.5, 21.6) ─────────
//
// operator<< MUST be non-member (left operand is std::ostream, not Fraction).
// Binary arith (+, *, Fraction*int) symmetric -> friend non-member.
// Comparison (==, !=, <) symmetric -> friend non-member.
// Unary (-) one operand -> member.
// Prefix/postfix ++ mutates the current object -> member.
//
// ── INVARIANT (same as Ch 14) ────────────────────────────────────────────
// m_denominator is always STRICTLY POSITIVE after construction.
// reduce() enforces this: negative denominator is absorbed into numerator;
// both parts are divided by their GCD. So equal fractions have IDENTICAL
// stored fields — operator== is a simple field comparison (notes 21.7).
//
// ── OVERFLOW GUARD (engineering note — not from the chapter) ──────────────
// operator< uses cross-multiplication: a/b < c/d iff a*d < b*c. The
// ordering idea is notes 21.7; the integer-overflow caution below is our
// own engineering aside (the note does not discuss it).
// The tests keep all values small (|num| ≤ 20, |den| ≤ 20) so a*d and b*c
// never leave the int range. Your code doesn't need a runtime overflow check,
// but never blindly cross-multiply large fractions without thinking of this.
//
// CS6340 lens: LLVM source is dense with overloaded operators — iterator
// comparison (!=, ++), stream diagnostics (<<), container subscript ([]).
// After this lab you will read that code without a second glance.
// ============================================================================
#ifndef FRACTION_H
#define FRACTION_H
#include <iostream> // std::ostream (for operator<<)
#include <string> // std::string, std::to_string (for toString())
class Fraction
{
public:
// ── Constructors (provided — same as Ch 14) ────────────────────────────
// Default: represents 0/1.
Fraction()
: m_numerator { 0 }
, m_denominator { 1 }
{}
// Two-argument: Fraction{3, 4} -> 3/4, normalized by reduce().
// If d == 0, falls back to n/1 (invalid input guard).
Fraction(int n, int d)
: m_numerator { n }
, m_denominator { d }
{
if (m_denominator == 0)
m_denominator = 1;
reduce();
}
// Single-argument whole number: explicit to block implicit int->Fraction.
explicit Fraction(int w)
: m_numerator { w }
, m_denominator { 1 }
{}
// ── Accessors (provided — same as Ch 14) ──────────────────────────────
int numerator() const { return m_numerator; }
int denominator() const { return m_denominator; }
bool isValid() const { return m_denominator != 0; }
// toString() — provided, used by tests for readable failure messages.
std::string toString() const
{
return std::to_string(m_numerator) + '/' + std::to_string(m_denominator);
}
// ─────────────────────────────────────────────────────────────────────────
// OPERATOR DECLARATIONS — these are what YOU implement in starter/fraction.cpp
// ─────────────────────────────────────────────────────────────────────────
// ── TASK 1: operator* (Fraction * Fraction) ───────────────────────────
// (a/b) * (c/d) = (a*c) / (b*d), then reduce.
// Symmetric -> friend non-member (notes 21.2).
friend Fraction operator*(const Fraction& lhs, const Fraction& rhs);
// ── TASK 2: operator* (Fraction * int) and (int * Fraction) ─────────
// Treat the int as a whole-number fraction n/1 (notes 21.2 mixed operands).
friend Fraction operator*(const Fraction& f, int n);
friend Fraction operator*(int n, const Fraction& f);
// ── TASK 3: operator+ ─────────────────────────────────────────────────
// (a/b) + (c/d) = (a*d + b*c) / (b*d), then reduce.
// Symmetric -> friend non-member.
friend Fraction operator+(const Fraction& lhs, const Fraction& rhs);
// ── TASK 4: operator== and operator!= ────────────────────────────────
// Because both fractions are fully reduced, equality is a field compare.
// != is expressed in terms of == (notes 21.7 — minimize redundancy).
friend bool operator==(const Fraction& lhs, const Fraction& rhs);
friend bool operator!=(const Fraction& lhs, const Fraction& rhs);
// ── TASK 5: operator< ─────────────────────────────────────────────────
// a/b < c/d iff a*d < b*c (cross-multiplication; denominators > 0
// so the inequality direction is preserved).
// Keep test values small — see overflow note in the file header.
friend bool operator<(const Fraction& lhs, const Fraction& rhs);
// ── TASK 6: unary operator- (negation) ────────────────────────────────
// Returns a new Fraction with the numerator sign flipped.
// One operand -> member function (notes 21.6).
Fraction operator-() const;
// ── TASK 7: prefix operator++ (add 1 = denominator/denominator) ───────
// Adds exactly 1 to the fraction: result = (num + den) / den, reduced.
// Prefix: mutates *this and returns *this by reference (notes 21.8).
Fraction& operator++();
// ── TASK 8: postfix operator++ (the dummy-int overload) ────────────────
// Returns the OLD value by value THEN increments.
// The dummy `int` parameter is the C++ syntax marker that distinguishes
// postfix from prefix — it is unused (the compiler passes 0 for the x++
// form); never read it in your body (notes 21.8).
Fraction operator++(int);
// ── TASK 9: operator<< (stream insertion) ─────────────────────────────
// Must be non-member: left operand is std::ostream, not Fraction (21.4).
// Output format: "num/den" e.g. Fraction{3,4} prints as "3/4".
// Return the stream by reference so chaining works: cout << a << b.
// Declared friend so it can read private m_numerator / m_denominator.
friend std::ostream& operator<<(std::ostream& out, const Fraction& f);
private:
int m_numerator {}; // carries the sign; may be negative, zero, positive
int m_denominator {}; // ALWAYS > 0 after construction (class invariant)
// ── Private helpers (provided — same as Ch 14) ──────────────────────
// gcd — Euclidean greatest common divisor on magnitudes.
// Private implementation detail of reduce().
int gcd(int a, int b) const
{
if (a < 0) a = -a;
if (b < 0) b = -b;
while (b != 0)
{
int tmp { b };
b = a % b;
a = tmp;
}
return a;
}
// reduce — enforce the class invariant in-place.
// 1. If denominator < 0, negate both parts so denominator > 0.
// 2. Divide both by gcd to produce lowest terms.
void reduce()
{
if (m_denominator < 0)
{
m_numerator = -m_numerator;
m_denominator = -m_denominator;
}
int g { gcd(m_numerator, m_denominator) };
if (g > 1)
{
m_numerator /= g;
m_denominator /= g;
}
}
};
#endif // FRACTION_H
# Chapter 21 — Operator Overloading · Fraction v2 · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Layout:
# fraction.h — the complete class declaration (declarations only)
# starter/fraction.cpp — the learner fills in the TASK blocks
# solution/fraction.cpp — reference implementation
# tests/tests.cpp — the grader; includes ../fraction.h
#
# The grader is compiled by linking tests/tests.cpp with EITHER starter or
# solution — the only difference is which -I path is searched for fraction.h.
# Both paths find the SAME header at the chapter root (not inside starter/ or
# solution/), so the header truly is the shared contract.
CXX := clang++
# -I. puts the chapter root on the include path so #include "../fraction.h"
# resolves correctly from both starter/ and tests/ subdirectories.
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── build — compile-check the learner's starter/fraction.cpp (warning-clean) ──
# The starter must compile out of the box so the learner can run make test
# immediately and see the RED failures.
build:
$(CXX) $(CXXFLAGS) -c starter/fraction.cpp -o starter/fraction.o
@echo "OK \xE2\x9C\x85 starter/fraction.cpp compiles. Run: make test"
# ── run — (no interactive program for this exercise; use make test) ────────────
run: build
@echo "No interactive driver — use: make test or make test-solution"
# ── test — grade the LEARNER's starter/fraction.cpp ───────────────────────────
# RED on the starter (stubs return wrong values); GREEN after all TASK blocks
# are correctly filled in.
test: tests/tests.cpp fraction.h starter/fraction.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp starter/fraction.cpp -o tests/run
@./tests/run || echo "FAIL \xE2\x9D\x8C fill in the TASK blocks in starter/fraction.cpp until every check passes."
# ── solution — build and show what the reference solution does ─────────────────
solution: solution/fraction.cpp fraction.h
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/fraction.cpp -o solution/run
./solution/run
# ── test-solution — proof the lab is solvable; MUST be green ──────────────────
test-solution: tests/tests.cpp fraction.h solution/fraction.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp solution/fraction.cpp -o tests/run
@./tests/run
clean:
rm -f starter/fraction.o tests/run solution/run
rm -rf tests/run.dSYM solution/run.dSYM
make test locally
(see “Build & run locally” above).