Skip to main content

Section 20.3 Basic Member Operators

A rational number represents a fraction like 3/4 or 5/2. Below is a simple class Rational to represent a rational number by storing a numerator and a denominator. None of this code is doing anything special yet, so don’t worry about reading it too closely.
#include <format>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <string>

using namespace std;

class Rational {
private:
    int m_numerator;
    int m_denominator;

public:
    Rational(int numerator, int denominator);

    int getNumerator() const;
    int getDenominator() const;
    double doubleValue() const;
    string toString() const;

    // Make a new Rational object that is the simplified version of this one
    Rational simplify() const;

    // Some functions we will build later...
    // Non-operator functions
    Rational add(const Rational& other) const;
    bool equals(const Rational& other) const;

    // Operators versions of those functions
    Rational operator+(const Rational& other) const;
    bool operator==(const Rational& other) const;

    // Other operators
    Rational& operator++();   // Prefix increment
    Rational operator++(int); // Postfix increment
    Rational& operator--();   // Prefix decrement
    Rational operator--(int); // Postfix decrement
};

Rational::Rational(int numerator, int denominator) {
    if (denominator == 0) {
        throw invalid_argument("Denominator cannot be zero.");
    }
    m_numerator = numerator;
    m_denominator = denominator;
}

int Rational::getNumerator() const {
    return m_numerator;
}

int Rational::getDenominator() const {
    return m_denominator;
}

double Rational::doubleValue() const {
    return static_cast<double>(m_numerator) / m_denominator;
}

string Rational::toString() const {
    string stringRep = format("{}/{}", m_numerator, m_denominator);
    return stringRep;
}

Rational Rational::simplify() const {
    int divisor = std::gcd(abs(m_numerator), abs(m_denominator));
    int newNumerator = m_numerator / divisor;
    int newDenominator = m_denominator / divisor;
    // - sign should only be in the numerator
    if (newDenominator < 0) {
        newNumerator = -newNumerator;
        newDenominator = -newDenominator;
    }
    return Rational(newNumerator, newDenominator);
}
Lines 30-34 declare some functions that are not implemented in the file. Those are what we will be focusing on.
Without operator overloading, we might build functions to add and compare Rational objects like this:
Listing 20.3.1.
Rational Rational::add(const Rational& other) const {
    int newNumerator = m_numerator * other.m_denominator 
                       + other.m_numerator * m_denominator;
    int newDenominator = m_denominator * other.m_denominator;
    Rational result(newNumerator, newDenominator);
    return result.simplify();
}

bool Rational::equals(const Rational& other) const {
    return m_numerator * other.m_denominator 
           == other.m_numerator * m_denominator;
}
The critical thing about these functions for our purposes is not the math (though you should take a few seconds to convince yourself that the answers will be correct), but the way the functions operate.
add is called on one Rational and given a second Rational as a parameter. It returns a new Rational that is the sum of the two without modifying either of the starting values. Thus the right way to use add on two Rationals named r1 and r2 would be:
Rational r3 = r1.add(r2);
Similarly, equals is called on one Rational and the other is passed as a parameter: r1.equals(r2) .
To make these into operators, we will keep the same functionality but change the function names to operator+ and operator==. This sample has those functions and a main to test them:
Listing 20.3.2.
When the compiler sees r1 + r2 and r1 == r2 in the above code, it translates them into r1.operator+(r2) and r1.operator==(r2). This is what we mean by syntactic sugar... the operators are really just regular member functions, but there is some syntax magic happening to make the code look simpler.
You can test this by changing r1 + r2 to r1.operator+(r2) in the main function. The code will run exactly the same!

Insight 20.3.1.

When the compiler sees something like data1 + data2, it will attempt to turn that into data1.operator+(data2).
The basic format we just learned for + can be applied to the other arithmetic symbols, such as -, *, and /. We also can take use == as a template to write other relational operators like !=, <, >=, etc... For each of the operators, we just need to figure out what logic can be done with the member variables to generate the correct answer.

Checkpoint 20.3.1.

Form the prototype for operator != as it would appear inside the class Rational.

Checkpoint 20.3.2.

Form the prototype for operator - as it would appear inside the class Rational.

Checkpoint 20.3.3.

Put the blocks in order to define a multiplication operator for the Rational class.
You have attempted of activities on this page.