Chapter 14 · Introduction to Classes
Exercise · Chapter 14

Chapter 14 — Introduction to Classes: The Fraction Class

You are building a Fraction class — a rational number stored as numerator / denominator — the chapter's own running example (notes 14.2, 14.8, 14.9). The class has a central invariant: the denominator is never zero and is always kept positive (the numerator carries any negative sign). Enforcing that invariant is the job of the class's constructors and private helpers; callers using the public interface cannot break it.

This is exactly the Chapter 14 central idea: a class wraps private data behind a public interface, uses constructors (with member-initializer lists) to guarantee valid initial state, and marks read-only operations as const member functions so they work on const objects and const references.

Because Chapter 14 introduces member bodies written inline (inside the class braces), you edit a single header, starter/fraction.h. The .h/.cpp member-definition split arrives in Chapter 15.

CS6340 tie-in: LLVM uses this pattern constantly. llvm::APInt stores an arbitrary-precision integer with a private limb array; its constructors normalize the representation, and many member functions are const. Once you can read a class with private data, member-init lists, and const member functions, you can read APInt.h — and the Instruction, Function, and Module class APIs that every analysis pass uses.

Your tasks

  1. Default constructor (0/1). Add a member-initializer list that sets m_numerator to 0 and m_denominator to 1. The body is empty. This is the simplest possible constructor; it just demonstrates the init-list syntax. (notes 14.10)

  2. Two-argument constructor Fraction(int n, int d). Use a member-init list to store n and d into m_numerator / m_denominator, then in the body: guard against d == 0 (set m_denominator = 1 if so), then call reduce() (already provided) to normalize the sign and divide out the GCD. After construction, Fraction{1,-2} stores -1/2; Fraction{4,6} stores 2/3. (notes 14.9, 14.10)

  3. Explicit whole-number constructor explicit Fraction(int w). Initialize m_numerator to w and m_denominator to 1 via the member-init list. The explicit keyword is already in the declaration skeleton — keep it. Whole numbers are already reduced, so no reduce() call is needed. (notes 14.16)

  4. Const accessor (getter) functions. Make numerator() return m_numerator and denominator() return m_denominator. Both must be marked const (the keyword already appears in the skeleton). Without const, calling them on a const Fraction or a const Fraction& fails at compile time. (notes 14.4)

  5. equals(const Fraction& other) const. Return true when m_numerator == other.m_numerator AND m_denominator == other.m_denominator. Because both fractions are fully reduced (constructors call reduce()), equal fractions have identical fields — no cross-multiplication needed. You may access other.m_numerator and other.m_denominator directly: same-class private access is legal inside member functions. (notes 14.5)

  6. multipliedBy(const Fraction& other) const. Compute (a/b) * (c/d) = (a*c) / (b*d) and return a new Fraction built with the two-argument constructor (which calls reduce() automatically). Do not modify *this. Return by value — C++17 copy elision (notes 14.15) makes this zero-overhead.

Success criteria

  • Fraction{4,6} must produce 2/3 (reduction working)
  • Fraction{1,-2} must produce -1/2 (sign normalization)
  • Fraction{-3,-4} must produce 3/4 (both signs cancel)
  • Fraction{0,7} must produce 0/1 (zero numerator)
  • Fraction{5,5} must produce 1/1 (numerator == denominator)
  • Fraction{3,0} must have a non-zero denominator (zero-denominator guard)
  • const Fraction cf{2,3}; cf.numerator() must compile (const member functions)
  • neg1.equals(neg2) where one is {-1,2} and the other is {1,-2} — both normalize to -1/2 so they must be equal
  • multipliedBy must leave the original object unchanged
  • (3/4) * (4/3) must produce 1/1, not 12/12 (reduce on the way out)
Concepts practiced
  • class keyword, public: / private: access specifiers (notes 14.2, 14.5)
  • Private data members with m_ prefix convention (notes 14.5)
  • Const member functions (notes 14.4) — required for const objects and const references, which are the standard way to pass class objects to functions
  • Constructors + member-initializer lists (notes 14.9, 14.10) — preferred over body assignment; certain members must use the list
  • explicit single-argument constructor (notes 14.16) — blocks accidental implicit int → Fraction conversions
  • Private helper functions (gcd, reduce) — encapsulating implementation details the caller never needs to see (notes 14.8)
  • Same-class private access inside member functions (notes 14.5) — used in equals() to read another Fraction's private fields
  • Return by value from a const member function (multipliedBy) — C++17 copy elision (notes 14.15) makes this free
  • Reused from earlier chapters: loops (while/if in gcd — Ch 8), int arithmetic (Ch 1), std::string + std::to_string (Ch 5), const references as parameters (Ch 5/12), header guards (Ch 2)
Constraints

Allowed:

  • class, public:, private:, member functions defined inside the class braces (the Ch 14 inline idiom)
  • const member functions (keyword after the parameter list)
  • Constructors with member-initializer lists
  • explicit on single-argument constructors
  • if / else, while loops, int arithmetic, % and / (Ch ≤ 8)
  • std::string, std::to_string, std::ostream (Ch 5, already included)
  • Same-class private access inside member functions

Forbidden (not yet taught):

  • ClassName::method definitions outside the class braces — that is a Ch 15 concept; keep all bodies inline
  • static members or functions (Ch 15)
  • operator==, operator*, or any other operator overloading (Ch 21) — use named methods like equals() and multipliedBy() instead
  • Destructors (Ch 15), inheritance (Ch 24), std::gcd from <numeric>
  • Changing the signatures of any provided functions

Required idioms:

  • Member-initializer lists (not body assignment) for all constructors (notes 14.10)
  • explicit on the single-argument whole-number constructor (notes 14.16)
  • const on every member function that does not modify the object (notes 14.4)
Build & run locally
shell
make            # compile-check starter/fraction.h  (already GREEN)
make test       # grade your class  ->  RED until the TASK blocks are filled
make solution   # compile and run the reference solution's tests
make clean      # remove build artifacts

make test compiles tests/tests.cpp with -Istarter so it picks up your header. make test-solution does the same with -Isolution — that is the Style B2 trick from CLAUDE.md: no .cpp files to link, just an include- path switch.

Hints
Task 1 — member-initializer list syntax
C++
Fraction()
    : m_numerator   { 0 }    // colon starts the init list
    , m_denominator { 1 }    // comma-separated; order matches declaration order
{
}                            // body is empty — everything was done in the list

The list runs before the constructor body. For simple values like int, this is equivalent to assigning in the body, but the list form is preferred (notes 14.10) because it initializes rather than default-initializes then assigns.

Task 2 — why call reduce() and what it does

reduce() (provided in the private section) does two things:

  1. If m_denominator < 0, it negates both parts so the denominator is positive.
  2. It computes gcd(m_numerator, m_denominator) and divides both by it.

You do not need to write this logic yourself — just call reduce() at the end of the two-argument constructor body. The member-init list sets the raw values; the body cleans them up:

C++
Fraction(int n, int d)
    : m_numerator   { n }
    , m_denominator { d }
{
    if (m_denominator == 0)
        m_denominator = 1;   // cannot store an invalid fraction
    reduce();                // normalize sign and reduce to lowest terms
}
Task 3 — what explicit actually blocks

Without explicit, a function declared as:

C++
bool isHalf(Fraction f);

could be called as:

C++
isHalf(2);   // int 2 is silently converted to Fraction{2} — surprising!

With explicit Fraction(int w), that implicit path is blocked. The caller must write isHalf(Fraction{2}) — the intent is visible. Best practice from notes 14.16: make single-argument constructors explicit by default unless you specifically want the implicit conversion.

Task 4 — why const is required, not optional

Given:

C++
void printFraction(const Fraction& f)
{
    std::cout << f.numerator();  // is this legal?
}

If numerator() is NOT marked const, the compiler refuses this call — even though numerator() obviously only reads. The rule: a const object (or const reference) may only call const member functions (notes 14.4). The const keyword is your promise to the compiler that the function does not modify the object. Without it, the compiler must assume the worst.

Task 5 — same-class private access

Inside any Fraction member function, you can access the private data of any Fraction object, not just *this:

C++
bool equals(const Fraction& other) const
{
    return (m_numerator   == other.m_numerator)   // other's PRIVATE field
        && (m_denominator == other.m_denominator);
}

Access control is per-class, not per-object (notes 14.5). Both other.m_numerator and m_numerator are private to Fraction, and you are inside Fraction's member function, so both accesses are legal.

Task 6 — multiply and why the result is already reduced
C++
Fraction multipliedBy(const Fraction& other) const
{
    return Fraction { m_numerator   * other.m_numerator,
                     m_denominator * other.m_denominator };
}

The two-argument Fraction(int, int) constructor calls reduce(), so the returned fraction is already in lowest terms — you do not need to call reduce() yourself. Return by value: C++17 copy elision (notes 14.15) constructs the result directly in the caller's storage, so there is no extra copy.

Note that you access other.m_numerator and other.m_denominator directly (same-class private access again), rather than going through the public accessors. Both styles are correct; direct field access is marginally faster and is the style the reference solution uses.

Stuck on a compile error before a test failure?
  • "error: 'numerator' is a private member" inside equals() or multipliedBy(): you may be trying to access other.numerator() via the public getter from a non-member context. Inside a Fraction member function, direct access to other.m_numerator is legal. Alternatively, calling other.numerator() (the public getter) is also legal — use whichever reads more clearly.
  • "error: member function 'numerator' not viable: 'this' argument has type 'const Fraction'": you forgot the trailing const on numerator() or denominator(). Add const after the closing ) of the parameter list.
  • "error: cannot convert 'int' to 'Fraction'": somewhere a raw int is being passed where a Fraction is expected. The explicit keyword on the one-arg constructor is doing its job. Wrap the int: Fraction{someInt}.
Stretch goals
  • Add a dividedBy(const Fraction& other) const method: (a/b) / (c/d) = (a*d) / (b*c). Guard against dividing by zero (other.numerator() == 0).
  • Add addedTo(const Fraction& other) const to practice the common-denominator algorithm without operator overloading. It is more complex than multipliedBy because you need lcm(d1, d2).
  • Overload operator* and operator== using the named methods above (Ch 21) to see how the named versions become the building blocks.
  • Extend the constructors to also accept a double and approximate the fraction (a fun continued-fractions exploration, though this goes well beyond Ch 14).
  • Replace isValid() with a compile-time static_assert in the constructor body using assert (Ch 9) to catch zero-denominator bugs during development.
starter/fraction.h C++
// Chapter 14 — Introduction to Classes · Project: The Fraction Class  (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// This is the ONLY file you edit. It contains the complete Fraction class with
// all member function bodies written INLINE — that is, inside the class braces.
// This is the Chapter 14 idiom; the .h/.cpp member split comes in Chapter 15.
//
// The file compiles immediately (the stubs return wrong-but-harmless values),
// so `make build` is GREEN right now. Your goal: fill in the six TASK blocks
// below until `make test` is also GREEN.
//
//     make build         compile-check this header (should pass immediately)
//     make test          grade your class  ->  RED until you fill these in
//     make test-solution run the grader against the reference if you get stuck
//
// ── CLASS DESIGN OVERVIEW ─────────────────────────────────────────────────────
//
//   A FRACTION represents the ratio numerator / denominator.
//
//   INVARIANT: denominator is never 0; we always keep it > 0 (denominator sign
//   is "absorbed" into the numerator so, e.g., 1/-2 is stored as -1/2).
//   Maintaining this INVARIANT is the class's main job — protecting it from
//   the outside world via PRIVATE data and a controlled PUBLIC interface.
//
//   CS6340 tie-in: LLVM uses classes like this everywhere. An `llvm::Fraction`
//   doesn't exist, but the idea — private data, public interface, invariant
//   maintenance via constructors — is exactly how `llvm::APInt` (arbitrary-
//   precision integer), `llvm::DebugLoc`, and many analysis result types work.
//
// ── SCOPE ─────────────────────────────────────────────────────────────────────
//   This header uses ONLY Chapter ≤ 14 features:
//     • class, public:/private:, member functions (Ch 14.2–14.5)
//     • const member functions (Ch 14.4)
//     • constructors + member-init lists (Ch 14.9–14.10)
//     • explicit one-arg constructor (Ch 14.16)
//     • loops, if/else (Ch 8)       — used in gcd() and reduce()
//   FORBIDDEN in this file: ClassName::method out-of-class definitions (Ch 15),
//   static members (Ch 15), operator overloading (Ch 21), destructors (Ch 15).
//   Use NAMED methods like equals() / multipliedBy(), not operator== / operator*.

#ifndef FRACTION_H
#define FRACTION_H

#include <iostream>    // std::ostream (used by print())
#include <string>      // std::string  (used by toString())

// ════════════════════════════════════════════════════════════════════════════
//  class Fraction
// ════════════════════════════════════════════════════════════════════════════
//
//  A RATIONAL NUMBER stored as (numerator, denominator) in reduced form with
//  denominator always POSITIVE (the numerator carries any negative sign).
//
//  Public interface (what callers can do):
//    Fraction{}             — constructs 0/1 (the additive identity)
//    Fraction{n, d}         — constructs n/d, normalized and reduced
//    explicit Fraction{w}   — constructs the whole number w/1; explicit
//                             prevents silent int→Fraction coercions
//    numerator()   const    — return the numerator
//    denominator() const    — return the denominator (always > 0)
//    isValid()     const    — true (denominator is always !=0, but still useful)
//    equals(other) const    — true when this == other (both fully reduced)
//    multipliedBy(other) const  — returns a NEW reduced Fraction (this * other)
//    print(os)     const    — prints "n/d" to the given stream
//    toString()    const    — returns "n/d" as a std::string
//
//  Private helper:
//    gcd(a, b)              — greatest common divisor (Euclidean algorithm)
//    reduce()               — reduces m_numerator / m_denominator in-place
//                             and ensures denominator is positive
class Fraction
{
public:
    // ── Member data (PRIVATE — callers cannot read or write these directly) ──
    // (Note: data is declared private at the bottom of the class. It can still
    // be listed here as a roadmap for readers.)

    // ─── TASK 1: Default constructor ─────────────────────────────────────────
    // Construct 0/1 — the "zero fraction". Use a MEMBER-INITIALIZER LIST
    // (the preferred C++ idiom — see notes 14.10). No body needed (empty {}).
    //
    // Pattern:
    //   Fraction()
    //       : m_numerator { 0 }    <-- list the members in DECLARATION order
    //       , m_denominator { 1 }  <-- a zero denominator would be invalid
    //   {
    //   }
    //
    //   >>> YOUR CODE HERE <<<
    //
    // ─────────────────────────────────────────────────────────────────────────
    Fraction()
        : m_numerator   { 0 }
        , m_denominator { 2 }   // placeholder: 0/2 is WRONG — fix the list to make 0/1
    {
        // body stays empty; do all the work in the member-initializer list above
    }

    // ─── TASK 2: Two-argument constructor ────────────────────────────────────
    // Construct n/d, normalized so denominator > 0, then reduced.
    //
    // Steps (all in the CONSTRUCTOR BODY — member-init list handles the raw
    // storage, then the body cleans it up):
    //   1. Member-init list: m_numerator{n}, m_denominator{d}.
    //   2. Body: guard against denominator 0 by setting m_denominator = 1 if
    //      d == 0.  (An invalid fraction becomes 0/1.)
    //   3. Call reduce() to normalize sign and divide out common factors.
    //
    // SIGN RULE (invariant): keep m_denominator POSITIVE; move any negative
    // sign into m_numerator. Example: Fraction{1,-2} stores -1/2.
    // reduce() handles this, so just call it.
    //
    //   >>> YOUR CODE HERE <<<
    //
    // ─────────────────────────────────────────────────────────────────────────
    Fraction(int /*n*/, int /*d*/)
        : m_numerator { 0 }
        , m_denominator { 1 }
    {
        // placeholder: always produces 0/1
    }

    // ─── TASK 3: Explicit one-argument constructor (whole-number conversion) ──
    // Construct the whole number w as w/1.
    //
    // WHY explicit? Without it, C++ allows IMPLICIT conversion: anywhere a
    // Fraction is expected, you could accidentally write just an int and the
    // compiler would silently construct a Fraction from it.  That is convenient
    // for well-known types like std::string (which accepts const char* implicitly)
    // but SURPRISING for Fraction, so we use the `explicit` keyword to require
    // the caller to write Fraction{5} explicitly. (notes 14.16)
    //
    //   >>> YOUR CODE HERE <<<
    //
    // ─────────────────────────────────────────────────────────────────────────
    explicit Fraction(int /*w*/)
        : m_numerator { 0 }
        , m_denominator { 1 }
    {
        // placeholder: always produces 0/1
    }

    // ─── TASK 4: const accessor (getter) functions ───────────────────────────
    // Return m_numerator and m_denominator respectively.
    // Marked `const` (after the parameter list) so they can be called on CONST
    // Fraction objects and through const references (notes 14.4).
    //
    //   >>> YOUR CODE HERE <<<
    //
    // ─────────────────────────────────────────────────────────────────────────
    int numerator() const
    {
        return 0;   // placeholder
    }

    int denominator() const
    {
        return 1;   // placeholder — 0 would be an illegal denominator
    }

    // isValid() — returns true when the denominator is non-zero.
    // Because our constructors guarantee denominator > 0, this always returns
    // true. Exposed as part of the public contract so callers can check.
    // Must be const (only reads, never modifies). (notes 14.4)
    bool isValid() const
    {
        return m_denominator != 0;
    }

    // ─── TASK 5: equals() — compare two Fractions ────────────────────────────
    // Return true when this fraction equals `other`.
    //
    // Because BOTH fractions are fully reduced (constructors call reduce()),
    // two equal fractions have IDENTICAL numerator AND denominator — so a
    // simple field comparison suffices.  Do NOT cross-multiply (unnecessary
    // and introduces overflow risk).
    //
    // Parameter type: `const Fraction& other` — const reference to avoid a
    // copy AND allow passing const Fractions (notes 14.4–14.5).
    //
    //   >>> YOUR CODE HERE <<<
    //
    // ─────────────────────────────────────────────────────────────────────────
    bool equals(const Fraction& /*other*/) const
    {
        return false;   // placeholder — never says equal (will fail most checks)
    }

    // ─── TASK 6: multipliedBy() — multiply two Fractions ─────────────────────
    // Return a NEW Fraction whose value is (this * other).
    //
    // Fraction multiplication: (a/b) * (c/d) = (a*c) / (b*d).
    // Construct and return a new Fraction with those products.
    // The two-argument constructor will call reduce(), so the result is
    // already in lowest terms. Do NOT mutate *this.
    //
    // Return type: Fraction (by VALUE — C++17 copy elision makes this free;
    // notes 14.15). Parameter: `const Fraction& other` (read-only, no copy).
    //
    //   >>> YOUR CODE HERE <<<
    //
    // ─────────────────────────────────────────────────────────────────────────
    Fraction multipliedBy(const Fraction& /*other*/) const
    {
        return Fraction{};   // placeholder — always returns 0/1
    }

    // ── Print helpers (provided, no task) ────────────────────────────────────
    // These are given to you so tests can display Fraction values. They use the
    // public accessors numerator() / denominator() only — no private access.

    // print() — write "n/d" to os (default: std::cout). const because it only
    // reads from the object. (notes 14.3, 14.4)
    void print(std::ostream& os = std::cout) const
    {
        os << numerator() << '/' << denominator();
    }

    // toString() — return "n/d" as a std::string for easy CHECK comparisons.
    std::string toString() const
    {
        return std::to_string(numerator()) + '/' + std::to_string(denominator());
    }

private:
    // ── PRIVATE DATA MEMBERS ─────────────────────────────────────────────────
    // The `m_` prefix is the LearnCpp / LLVM codebase convention for private
    // data members (notes 14.5). Callers cannot read or write these directly.
    int m_numerator {};    // numerator; carries the sign (may be negative)
    int m_denominator {};  // denominator; ALWAYS positive (enforced by reduce())

    // ── PRIVATE HELPER FUNCTIONS ─────────────────────────────────────────────

    // gcd — Greatest Common Divisor via the Euclidean algorithm.
    // This is a PRIVATE member function; callers outside the class cannot use it.
    // Returns the GCD of |a| and |b|; gcd(0, n) == n by convention.
    // We roll our own loop (not std::gcd from <numeric>) because <numeric>
    // advanced features are taught later. (loop skill: Ch 8)
    int gcd(int a, int b) const
    {
        if (a < 0) a = -a;   // work on magnitudes
        if (b < 0) b = -b;
        while (b != 0)
        {
            int tmp { b };
            b = a % b;
            a = tmp;
        }
        return a;    // when b reaches 0, a holds the GCD
    }

    // reduce — normalize in-place: ensure denominator > 0, divide out the GCD.
    // Called at the end of any constructor that accepts arbitrary arguments.
    // Keeping this in reduce() centralizes the invariant-maintenance code.
    void reduce()
    {
        // Step 1: keep denominator positive (move sign to numerator).
        if (m_denominator < 0)
        {
            m_numerator   = -m_numerator;
            m_denominator = -m_denominator;
        }
        // Step 2: divide out the greatest common divisor so the fraction is
        // already in lowest terms when callers receive it.
        int g { gcd(m_numerator, m_denominator) };
        if (g > 1)
        {
            m_numerator   /= g;
            m_denominator /= g;
        }
    }
};

#endif // FRACTION_H
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).