Chapter 21 · Operator Overloading
Exercise · Chapter 21

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:

C++
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 build compiles starter/fraction.cpp with zero warnings under -Wall -Wextra.
  • make test exits GREEN — all checks pass.
  • You can explain why postfix x++ and prefix ++x return different things, and why operator<< cannot be a member of Fraction.
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 *this by 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)
  • const member functions, access specifiers, explicit (Chapter 14)
  • Friend declarations for private-data access (Chapter 15)
  • std::string and std::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 new Fraction
  • Delegating operator!= to operator==; delegating int*Fraction to Fraction*int
  • Delegating postfix ++ to prefix ++

Forbidden:

  • operator<=> (C++20 spaceship — explicitly out of scope for this course level)
  • operator[] and operator() (subscript/call operators — out of scope for this exercise)
  • Conversion operators (operator int() etc.) — out of scope
  • Altering fraction.h, tests/tests.cpp, or Makefile
  • 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 return std::ostream& (for chaining)

Build & run locally
shell
# 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 clean

Hints
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:

C++
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:

C++
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.

C++
Fraction& Fraction::operator++()
{
    m_numerator += m_denominator;
    reduce();
    return *this;   // ref to the updated object
}

Postfix (x++): save old, increment, return old.

C++
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:

C++
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.

  1. operator- (binary subtraction): a - b = a + (-b). Delegate to operator- (unary) and operator+.

  2. operator+=: a compound-assignment variant. Should modify *this and return *this&. Then rewrite operator+ in terms of operator+= using a copy (the idiomatic implementation order).

  3. operator>, operator<=, operator>=: derive all three from operator< using the same minimize-redundancy principle.

  4. operator>> (stream extraction, Chapter 28 I/O): read a fraction from "num/den" input, validate that denominator is non-zero, set failbit on bad input. (Formally a Chapter 28 concept — notes 21.4 mentions it briefly.)

  5. std::sort compatibility: since operator< is defined, a std::vector<Fraction> can be sorted with std::sort (Chapter 18 algorithms). Try it.

starter/fraction.cpp C++
// 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;
}
Run
Submit
Run in your browser — coming soon For now: copy or download the files and use make test locally (see “Build & run locally” above).