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
Default constructor (0/1). Add a member-initializer list that sets
m_numeratorto0andm_denominatorto1. The body is empty. This is the simplest possible constructor; it just demonstrates the init-list syntax. (notes 14.10)Two-argument constructor
Fraction(int n, int d). Use a member-init list to storenanddintom_numerator/m_denominator, then in the body: guard againstd == 0(setm_denominator = 1if so), then callreduce()(already provided) to normalize the sign and divide out the GCD. After construction,Fraction{1,-2}stores-1/2;Fraction{4,6}stores2/3. (notes 14.9, 14.10)Explicit whole-number constructor
explicit Fraction(int w). Initializem_numeratortowandm_denominatorto1via the member-init list. Theexplicitkeyword is already in the declaration skeleton — keep it. Whole numbers are already reduced, so noreduce()call is needed. (notes 14.16)Const accessor (getter) functions. Make
numerator()returnm_numeratoranddenominator()returnm_denominator. Both must be markedconst(the keyword already appears in the skeleton). Withoutconst, calling them on aconst Fractionor aconst Fraction&fails at compile time. (notes 14.4)equals(const Fraction& other) const. Returntruewhenm_numerator == other.m_numeratorANDm_denominator == other.m_denominator. Because both fractions are fully reduced (constructors callreduce()), equal fractions have identical fields — no cross-multiplication needed. You may accessother.m_numeratorandother.m_denominatordirectly: same-class private access is legal inside member functions. (notes 14.5)multipliedBy(const Fraction& other) const. Compute(a/b) * (c/d) = (a*c) / (b*d)and return a newFractionbuilt with the two-argument constructor (which callsreduce()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 produce2/3(reduction working)Fraction{1,-2}must produce-1/2(sign normalization)Fraction{-3,-4}must produce3/4(both signs cancel)Fraction{0,7}must produce0/1(zero numerator)Fraction{5,5}must produce1/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/2so they must be equalmultipliedBymust leave the original object unchanged(3/4) * (4/3)must produce1/1, not12/12(reduce on the way out)
Concepts practiced
classkeyword,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
explicitsingle-argument constructor (notes 14.16) — blocks accidental implicitint → Fractionconversions- 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 anotherFraction'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/ifingcd— Ch 8),intarithmetic (Ch 1),std::string+std::to_string(Ch 5),constreferences 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)constmember functions (keyword after the parameter list)- Constructors with member-initializer lists
expliciton single-argument constructorsif/else,whileloops,intarithmetic,%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::methoddefinitions outside the class braces — that is a Ch 15 concept; keep all bodies inlinestaticmembers or functions (Ch 15)operator==,operator*, or any other operator overloading (Ch 21) — use named methods likeequals()andmultipliedBy()instead- Destructors (Ch 15), inheritance (Ch 24),
std::gcdfrom<numeric> - Changing the signatures of any provided functions
Required idioms:
- Member-initializer lists (not body assignment) for all constructors (notes 14.10)
expliciton the single-argument whole-number constructor (notes 14.16)conston every member function that does not modify the object (notes 14.4)
Build & run locally
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 artifactsmake 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
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 listThe 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:
- If
m_denominator < 0, it negates both parts so the denominator is positive. - 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:
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:
bool isHalf(Fraction f);could be called as:
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:
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:
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
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()ormultipliedBy(): you may be trying to accessother.numerator()via the public getter from a non-member context. Inside aFractionmember function, direct access toother.m_numeratoris legal. Alternatively, callingother.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
constonnumerator()ordenominator(). Addconstafter the closing)of the parameter list. - "error: cannot convert 'int' to 'Fraction'": somewhere a raw
intis being passed where aFractionis expected. Theexplicitkeyword on the one-arg constructor is doing its job. Wrap the int:Fraction{someInt}.
Stretch goals
- Add a
dividedBy(const Fraction& other) constmethod:(a/b) / (c/d) = (a*d) / (b*c). Guard against dividing by zero (other.numerator() == 0). - Add
addedTo(const Fraction& other) constto practice the common-denominator algorithm without operator overloading. It is more complex thanmultipliedBybecause you needlcm(d1, d2). - Overload
operator*andoperator==using the named methods above (Ch 21) to see how the named versions become the building blocks. - Extend the constructors to also accept a
doubleand approximate the fraction (a fun continued-fractions exploration, though this goes well beyond Ch 14). - Replace
isValid()with a compile-timestatic_assertin the constructor body usingassert(Ch 9) to catch zero-denominator bugs during development.
// 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
Try the lab first — the learning is in the attempt.
// Chapter 14 — Introduction to Classes · Project: The Fraction Class (SOLUTION)
// ─────────────────────────────────────────────────────────────────────────────
// Reference implementation. Peek only after a real attempt at starter/fraction.h.
// The learning is in designing the constructors and const-member-function
// signatures yourself; then compare here to see if your intuitions held.
//
// All member bodies are INLINE (defined inside the class braces) — the Ch 14
// idiom. The .h/.cpp split (ClassName::method out-of-class defs) arrives in
// Chapter 15. Everything here uses only Chapter ≤ 14 concepts.
//
// ── CLASS DESIGN NOTES ────────────────────────────────────────────────────────
//
// INVARIANT: m_denominator is always STRICTLY POSITIVE.
// We absorb any negative sign from the denominator into the numerator so that
// the reduced form is unique: 2/3 and -2/3 are each their own canonical form.
// This makes equals() a simple field comparison — no cross-multiplication.
//
// reduce() is the single place that enforces the invariant; it is called at
// the end of every constructor that accepts caller-supplied values. The
// private visibility of gcd() and reduce() is important: callers outside the
// class have no way to put the fraction into an un-reduced state.
//
// CS6340 tie-in: LLVM's APInt uses a similar "normalize-on-construction"
// pattern. llvm::Fraction itself doesn't exist, but every class with a
// "canonical" internal form encapsulates the normalization exactly like this.
#ifndef FRACTION_H
#define FRACTION_H
#include <iostream> // std::ostream
#include <string> // std::string, std::to_string
class Fraction
{
public:
// ─── TASK 1: Default constructor (0/1) ───────────────────────────────────
// MEMBER-INITIALIZER LIST (notes 14.10): initialize members BEFORE the
// constructor body runs. For simple value types like Fraction, the list is
// the whole story — the body is empty. List members in DECLARATION ORDER
// (m_numerator first, then m_denominator) to match init order.
Fraction()
: m_numerator { 0 }
, m_denominator { 1 }
{
}
// ─── TASK 2: Two-argument constructor ────────────────────────────────────
// The member-init list stores the raw arguments; the body guards the zero-
// denominator case and then calls reduce() to enforce the invariant.
//
// WHY NOT put the guard in the init list?
// We could write: m_denominator { d == 0 ? 1 : d }
// but that leaves the sign-normalization to reduce() anyway, so keeping
// all invariant work in the body + reduce() is cleaner.
Fraction(int n, int d)
: m_numerator { n }
, m_denominator { d }
{
if (m_denominator == 0)
m_denominator = 1; // invalid input -> fall back to n/1
reduce(); // normalize sign, divide out GCD
}
// ─── TASK 3: Explicit whole-number constructor ────────────────────────────
// `explicit` blocks IMPLICIT conversion (notes 14.16). Without it:
//
// bool isHalf(Fraction f);
// isHalf(2); // silently constructs Fraction{2} — surprising!
//
// With `explicit`, the caller MUST write Fraction{2} — the intent is clear.
// Single-argument constructors should default to `explicit` unless you
// specifically WANT the implicit conversion (e.g., std::string from const char*).
//
// Whole number w is stored as w/1 — already reduced, so no reduce() needed.
explicit Fraction(int w)
: m_numerator { w }
, m_denominator { 1 }
{
}
// ─── TASK 4: Const accessor (getter) functions ───────────────────────────
// The `const` AFTER the parameter list is the key syntax (notes 14.4):
// return_type fn_name(params) const { ... }
//
// It promises the function will NOT modify the implicit object. This lets
// the function be called on const Fraction objects and on const Fraction& params.
//
// Without const, passing a Fraction by const reference and then calling
// numerator() would fail at compile time — even though the function
// "obviously" only reads. The compiler enforces const correctness strictly.
int numerator() const
{
return m_numerator;
}
int denominator() const
{
return m_denominator;
}
// isValid() — always true (constructors guarantee m_denominator > 0).
// Useful as a sanity assertion in callers and as a teaching example.
bool isValid() const
{
return m_denominator != 0;
}
// ─── TASK 5: equals() — same-class private access ─────────────────────────
// Both fractions are fully reduced, so equal fractions have IDENTICAL
// numerator and denominator. No cross-multiplication needed (and
// cross-multiplying could overflow for large values).
//
// KEY INSIGHT (notes 14.5): objects of the SAME class may access each
// other's private members inside member functions. Here, `other.m_numerator`
// and `other.m_denominator` are private, but we are inside Fraction's
// member function, so the access is legal. This pattern shows up constantly
// in LLVM: one Instruction examining another's private flags.
bool equals(const Fraction& other) const
{
return (m_numerator == other.m_numerator)
&& (m_denominator == other.m_denominator);
}
// ─── TASK 6: multipliedBy() — return a NEW Fraction ──────────────────────
// Fraction multiplication: (a/b) * (c/d) = (a*c) / (b*d).
//
// We construct a NEW Fraction via the two-arg constructor — which calls
// reduce() — so the result is already in lowest terms. `*this` is not
// modified (const guarantees that). The return is by value; C++17 copy
// elision (notes 14.15) means the Fraction is constructed directly in the
// caller's storage — no copy overhead.
//
// Like equals(), we access other.m_numerator / other.m_denominator directly
// (same-class private access).
Fraction multipliedBy(const Fraction& other) const
{
return Fraction { m_numerator * other.m_numerator,
m_denominator * other.m_denominator };
}
// ── Print helpers (provided as scaffolding, no task) ─────────────────────
void print(std::ostream& os = std::cout) const
{
os << m_numerator << '/' << m_denominator;
}
std::string toString() const
{
return std::to_string(m_numerator) + '/' + std::to_string(m_denominator);
}
private:
// ── PRIVATE DATA MEMBERS ──────────────────────────────────────────────────
// `m_` prefix: the LearnCpp / LLVM naming convention for private data.
// Declared AFTER the public interface; readers see the API first (notes 14.8).
int m_numerator {}; // carries the sign; may be negative, zero, or positive
int m_denominator {}; // ALWAYS > 0 after construction (invariant)
// ── PRIVATE HELPERS ───────────────────────────────────────────────────────
// gcd — Euclidean algorithm for Greatest Common Divisor.
// Private because callers have no reason to use it; it is an implementation
// detail of reduce(). We hand-roll the loop rather than calling std::gcd
// (from <numeric>) because that header's advanced features are taught later.
//
// gcd(0, n) == n, gcd(n, 0) == n by convention.
int gcd(int a, int b) const
{
if (a < 0) a = -a; // Euclidean GCD works on magnitudes
if (b < 0) b = -b;
while (b != 0)
{
int tmp { b }; // rotate (a, b) -> (b, a % b); plain ints, no library needed
b = a % b;
a = tmp;
}
return a;
}
// reduce — enforce the class invariant in-place.
// 1. If denominator is negative, negate both parts so denominator > 0.
// 2. Divide both parts by gcd to produce the lowest-terms fraction.
//
// Called at the end of the two-argument constructor (the default and
// explicit one-arg constructors already produce valid, reduced fractions
// without needing this).
void reduce()
{
// Invariant part 1: denominator must be positive.
if (m_denominator < 0)
{
m_numerator = -m_numerator;
m_denominator = -m_denominator;
}
// Invariant part 2: no common factors (lowest terms).
// gcd(0, d) == d and gcd(n, 1) == 1, so edge cases are safe.
int g { gcd(m_numerator, m_denominator) };
if (g > 1)
{
m_numerator /= g;
m_denominator /= g;
}
}
};
#endif // FRACTION_H
// Chapter 14 — Introduction to Classes · Project: The Fraction Class (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// Tiny no-framework unit-test harness (matches the CLAUDE.md spec). Picks up
// the learner's header via -Istarter (make test) or the reference via -Isolution
// (make test-solution) — that path-switch IS the Style B2 trick (CLAUDE.md §B2).
//
// CHECKs are grouped by task. Each group tests the HAPPY PATH (typical inputs)
// and at least one EDGE CASE (zero, negative, same-class access, etc.).
//
// Do NOT edit this file — it is the grader contract.
#include <iostream>
#include <string>
#include "fraction.h" // resolved to starter/ or solution/ via -I on the command line
static int fails = 0;
// CHECK: if cond is false, print the failing expression + line number.
#define CHECK(cond) \
do { if (!(cond)) { \
std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; \
++fails; \
} } while (0)
int main()
{
// ── Task 1: Default constructor ───────────────────────────────────────────
// Fraction{} must produce 0/1 (the "additive zero" identity).
{
Fraction z {};
CHECK(z.numerator() == 0);
CHECK(z.denominator() == 1);
CHECK(z.isValid());
CHECK(z.toString() == "0/1");
}
// ── Task 2: Two-argument constructor — normal cases ───────────────────────
{
Fraction half { 1, 2 };
CHECK(half.numerator() == 1);
CHECK(half.denominator() == 2);
CHECK(half.isValid());
}
{
Fraction neg { -3, 4 };
CHECK(neg.numerator() == -3);
CHECK(neg.denominator() == 4); // denominator stays positive
}
// ── Task 2: Two-argument constructor — sign normalization ─────────────────
// Fraction{1,-2} must store as -1/2 (denominator always positive).
{
Fraction negDenom { 1, -2 };
CHECK(negDenom.numerator() == -1);
CHECK(negDenom.denominator() == 2);
}
// Fraction{-3,-4} = 3/4 (both signs cancel, result is positive).
{
Fraction negBoth { -3, -4 };
CHECK(negBoth.numerator() == 3);
CHECK(negBoth.denominator() == 4);
}
// ── Task 2: Two-argument constructor — reduction ──────────────────────────
// 4/6 reduces to 2/3; 6/4 reduces to 3/2; 0/7 reduces to 0/1.
{
Fraction r1 { 4, 6 };
CHECK(r1.numerator() == 2);
CHECK(r1.denominator() == 3);
}
{
Fraction r2 { 6, 4 };
CHECK(r2.numerator() == 3);
CHECK(r2.denominator() == 2);
}
{
// Edge: 0/7 — numerator is 0; fraction is 0/1 after reduction.
Fraction zero { 0, 7 };
CHECK(zero.numerator() == 0);
CHECK(zero.denominator() == 1);
}
{
// Edge: numerator equals denominator -> 1/1.
Fraction one { 5, 5 };
CHECK(one.numerator() == 1);
CHECK(one.denominator() == 1);
}
{
// Edge: bad denominator 0 -> fall back to n/1 (per spec).
Fraction bad { 3, 0 };
CHECK(bad.isValid());
CHECK(bad.denominator() != 0);
}
{
// Edge: negative numerator with denominator that shares a factor.
// -6/-4 => sign cancel => 6/4 => reduces to 3/2.
Fraction r3 { -6, -4 };
CHECK(r3.numerator() == 3);
CHECK(r3.denominator() == 2);
}
// ── Task 3: Explicit whole-number constructor ─────────────────────────────
// Fraction{5} must build 5/1. The `explicit` keyword prevents implicit
// conversion: the tests below must use explicit construction syntax.
{
Fraction five { Fraction(5) };
CHECK(five.numerator() == 5);
CHECK(five.denominator() == 1);
}
{
Fraction neg { Fraction(-3) };
CHECK(neg.numerator() == -3);
CHECK(neg.denominator() == 1);
}
{
// Edge: whole-number zero.
Fraction z { Fraction(0) };
CHECK(z.numerator() == 0);
CHECK(z.denominator() == 1);
}
// ── Task 4: const member functions on const objects ───────────────────────
// A const Fraction must be callable for read-only accessors.
{
const Fraction cf { 2, 3 };
CHECK(cf.numerator() == 2);
CHECK(cf.denominator() == 3);
CHECK(cf.isValid());
}
{
// Through a const reference (the common call site in real code).
const Fraction& cref { Fraction(7, 8) };
CHECK(cref.numerator() == 7);
CHECK(cref.denominator() == 8);
}
// ── Task 5: equals() ──────────────────────────────────────────────────────
{
Fraction a { 1, 2 };
Fraction b { 1, 2 };
CHECK(a.equals(b)); // same fraction
CHECK(b.equals(a)); // symmetric
Fraction c { 2, 3 };
CHECK(!a.equals(c)); // different fractions
// Two representations of the same value must both reduce to 2/3.
Fraction d { 4, 6 };
Fraction e { 2, 3 };
CHECK(d.equals(e)); // edge: 4/6 == 2/3 after reduction
// Whole-number vs fraction form.
Fraction whole { Fraction(1) };
Fraction frac { 3, 3 };
CHECK(whole.equals(frac)); // 1/1 == 3/3-reduced = 1/1
// Zero equality.
Fraction z1 { 0, 5 };
Fraction z2 {};
CHECK(z1.equals(z2)); // edge: 0/5 reduces to 0/1 == 0/1
// Negative fractions.
Fraction neg1 { -1, 2 };
Fraction neg2 { 1, -2 }; // stores as -1/2
CHECK(neg1.equals(neg2)); // both should reduce to -1/2
}
// ── Task 6: multipliedBy() ────────────────────────────────────────────────
{
// (1/2) * (2/3) = 2/6 = 1/3.
Fraction half { 1, 2 };
Fraction third { 2, 3 };
Fraction result { half.multipliedBy(third) };
CHECK(result.numerator() == 1);
CHECK(result.denominator() == 3);
CHECK(result.equals(Fraction { 1, 3 }));
}
{
// (3/4) * (4/3) = 12/12 = 1/1.
Fraction a { 3, 4 };
Fraction b { 4, 3 };
CHECK(a.multipliedBy(b).equals(Fraction(1))); // whole-number result
}
{
// Edge: multiply by 0 -> 0/1.
Fraction a { 1, 2 };
Fraction zero {};
Fraction r { a.multipliedBy(zero) };
CHECK(r.numerator() == 0);
CHECK(r.denominator() == 1);
}
{
// Edge: multiply two negatives -> positive.
// (-1/2) * (-2/3) = 2/6 = 1/3.
Fraction a { -1, 2 };
Fraction b { -2, 3 };
Fraction r { a.multipliedBy(b) };
CHECK(r.numerator() == 1);
CHECK(r.denominator() == 3);
}
{
// Edge: multiply whole number * fraction.
Fraction two { Fraction(2) }; // 2/1
Fraction half { 1, 2 }; // 1/2
CHECK(two.multipliedBy(half).equals(Fraction(1))); // 2/2 = 1/1
}
{
// Original object must be UNCHANGED (multipliedBy is const, returns new).
Fraction orig { 3, 4 };
Fraction other { 2, 5 };
Fraction r { orig.multipliedBy(other) };
CHECK(orig.numerator() == 3); // orig untouched
CHECK(orig.denominator() == 4);
CHECK(r.numerator() == 3); // 6/20 reduces to 3/10
CHECK(r.denominator() == 10);
}
// ── toString / print helpers (provided scaffolding, just sanity-check) ────
{
Fraction f { 3, 5 };
CHECK(f.toString() == "3/5");
Fraction neg { -1, 4 };
CHECK(neg.toString() == "-1/4");
}
// ── Summary ───────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all Fraction checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails
<< " check(s) failed — fix the TASK blocks in fraction.h.\n";
return fails ? 1 : 0;
}
# Chapter 14 — Introduction to Classes · The Fraction Class · Style B2 grader.
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# Style B2 (header-only): the learner edits starter/fraction.h — the full class
# with all member bodies INLINE (the ch14 idiom; .h/.cpp split comes in ch15).
# The grader switches include paths (-Istarter vs -Isolution) instead of linking
# different .cpp files. tests/tests.cpp does #include "fraction.h" and picks up
# whichever directory is on -I. No demo main is separately built for this style.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# build — compile-check the LEARNER's header (starter/fraction.h).
# We compile the tests against the starter so that any error is surfaced early.
# This target deliberately succeeds even though the tests will FAIL at runtime;
# the "RED" behavior is a *runtime* fail, not a compile error.
build:
$(CXX) $(CXXFLAGS) -Istarter tests/tests.cpp -o tests/run_starter
@echo "OK ✅ starter/fraction.h compiles. Now run: make test"
# run — run the starter grader so the learner sees the red output interactively.
run: build
@./tests/run_starter || true
# test — grade the LEARNER's header: RED until the TASK blocks are filled in.
test:
$(CXX) $(CXXFLAGS) -Istarter tests/tests.cpp -o tests/run_starter
@./tests/run_starter || echo "FAIL ❌ fill in the TASK blocks in starter/fraction.h until every check passes."
# solution — compile and run the REFERENCE solution as a quick sanity demo.
solution:
$(CXX) $(CXXFLAGS) -Isolution tests/tests.cpp -o tests/run_solution
@./tests/run_solution
# test-solution — proof the lab is solvable: solution MUST pass every check.
test-solution:
$(CXX) $(CXXFLAGS) -Isolution tests/tests.cpp -o tests/run_solution
@./tests/run_solution
clean:
rm -f tests/run_starter tests/run_solution
rm -rf tests/run_starter.dSYM tests/run_solution.dSYM
make test locally
(see “Build & run locally” above).