Skip to main content
Minor improvements to my code
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308

Normally though, we would just let the compiler generate that one:

Rational::Rational(const Rational& rat) = default;
#ifndef _RATIONAL_H_
#define _RATIONAL_H_

#include <iosfwd>
#include <stdexcept>
#include <type_traits>
#include <utility>

class Rational
{
    long long p;
    long long q;

    constexpr Rational& simplify();

public:
    template<typename T> constexpr Rational(T p, T q = 1) requires std::is_integral<T>::value;
    constexpr Rational(unsigned long long p = 0, unsigned long long q = 1);
    constexpr Rational(const Rational&);

    // assignment operators
    constexpr Rational& operator=(const Rational&);
    constexpr Rational& operator+=(const Rational&);
    constexpr Rational& operator-=(const Rational&);
    constexpr Rational& operator*=(const Rational&);
    constexpr Rational& operator/=(const Rational&);

    // comparison operators
    friend constexpr bool operator==(const Rational&, const Rational&);
    friend constexpr bool operator<(const Rational&, const Rational&);

    // increment and decrement operators
    constexpr Rational operator++(int);
    constexpr Rational operator--(int);
    constexpr Rational& operator++();
    constexpr Rational& operator--();

    // type conversion
    constexpr explicit operator double(); const;

    // stream operators
    friend std::ostream& operator<<(std::ostream&, const Rational&);
    friend std::istream& operator>>(std::istream&, Rational&);

    // arithmetic functions
    constexpr Rational pow(unsigned exp) const;
    constexpr Rational inverse() const;
};

// arithmetic operators
constexpr Rational operator+(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&, const Rational&);
constexpr Rational operator*(const Rational&, const Rational&);
constexpr Rational operator/(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&);
constexpr Rational operator+(const Rational&);


template<typename T>
constexpr Rational::Rational(T p, T q) requires (std::is_integral<T>::value)
    : p{p},
      q{q}
{
    if (q == 0)
        throw std::domain_error{"zero Denominator"};
    simplify();
}

#endif
#include "rational.h"
#include <iostream>
#include <limits>
    
constexpr Rational operator""_r(unsigned long long p)
{
    // default conversion
    return p;
}
 

namespace {
    constexpr long long gcd(long long p, long long q)
    {
        returnwhile (q) ?{
 gcd           p %= q;
            std::swap(qp, p%qq);
 :       }
        return p;
    }
}

constexpr Rational::Rational(unsigned long long p, unsigned long long q)
    : Rational{static_cast<long long>(p), static_cast<long long>(q)}
{
    // Retrospectively justify static_cast<> above
    constexpr unsigned long long max_ll = std::numeric_limits<long long>::max();
    if (p > max_ll || q > max_ll)
        throw std::domain_error{"value out of range"};
}

constexpr Rational::Rational(const Rational& rat)
    : Rational{rat.p,= rat.q}
{
}default;

constexpr Rational& Rational::simplify()
{
    // Fix negative denominators
    if (q < 0) {
        p = -p;
        q = -q;
    }

    // Reduce by greatest common divisor.
    // N.B. if p==0, this results in 0/1 as desired.
    const auto denom = gcd(p, q);
    p /= denom;
    q /= denom;

    return *this;
}

constexpr Rational& Rational::operator=(const Rational& rat)
{
    p = rat.p;
    q = rat.q;
    return *this;
}

constexpr Rational& Rational::operator+=(const Rational& rat)
{
    p = p * rat.q + q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator-=(const Rational& rat)
{
    p = p * rat.q - q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator*=(const Rational& rat)
{
    p *= rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator/=(const Rational& rat)
{
    if (rat.p == 0)
        throw std::domain_error{"Division by zero not allowed"};
    return *this *= rat.inverse();
}


constexpr Rational operator+(const Rational& a, const Rational& b)
{
    return Rational{a} += b;
}

constexpr Rational operator-(const Rational& a, const Rational& b)
{
    return Rational{a} -= b;
}

constexpr Rational operator*(const Rational& a, const Rational& b)
{
    return Rational{a} *= b;
}

constexpr Rational operator/(const Rational& a, const Rational& b)
{
    return Rational{a} /= b;
}

constexpr Rational operator-(const Rational& r)
{
    return 0 - r;
}

constexpr Rational operator+(const Rational& r)
{
    return r;
}


constexpr bool operator==(const Rational& a, const Rational& b)
{
    return a.p == b.p && a.q == b.q;
}

constexpr bool operator<(const Rational& a, const Rational& b)
{
    return a.p * b.q  <  a.q * b.p;
}


constexpr Rational Rational::operator++(int) // Postfix
{
    Rational temp{*this};
    p += q;
    return temp;
}

constexpr Rational Rational::operator--(int) // Postfix
{
    Rational temp{*this};
    p -= q;
    return temp;
}

constexpr Rational& Rational::operator++()
{
    return *this += 1;
}

constexpr Rational& Rational::operator--()
{
    return *this -= 1;
}

constexpr Rational::operator double() const
{
    return static_cast<double>(p) / static_cast<double>(q);
}


std::ostream& operator<<(std::ostream& os, const Rational& rat)
{
    return os << rat.p << ":" << rat.q;
}

std::istream& operator>>(std::istream& is, Rational& rat)
{
    long long p, q;
    char sep;
    if (is >> p >> sep >> q && sep == ':')
        rat = {p, q};
    return is;
}

constexpr Rational Rational::pow(unsigned exp) const
{
    auto x = *this;
    Rational r{1};
    for (;  exp;  exp /= 2) {
        if (exp%2) r *= x;
        x *= x;
    }
    return r;
}

constexpr Rational Rational::inverse() const
{
    if (p < 0) {
        // Make the denominator positive
        return {-q, -p};
    } else {
        return {q, p};
    }
}



//************************************************************************************
// Test Code starts here

#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);

    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1L + 1/4_r, 5/4_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/3_r - 1/3_r, 0_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);
    errors += TEST_EQUAL(1/3_r * 0_r, 0_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);
    errors += TEST_EQUAL((-2/3_r).inverse(), -3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}

Tests


Further suggestions

#include "rational.h"
#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);


    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1L + 1/4_r, 5/4_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}

At present, there's no check for overflow in any of the arithmetic operations. This can happen surprisingly quickly in rational arithmetic, so consider how you might detect or avoid it.

#ifndef _RATIONAL_H_
#define _RATIONAL_H_

#include <iosfwd>
#include <stdexcept>
#include <type_traits>
#include <utility>

class Rational
{
    long long p;
    long long q;

    constexpr Rational& simplify();

public:
    template<typename T> constexpr Rational(T p, T q = 1) requires std::is_integral<T>::value;
    constexpr Rational(unsigned long long p = 0, unsigned long long q = 1);
    constexpr Rational(const Rational&);

    // assignment operators
    constexpr Rational& operator=(const Rational&);
    constexpr Rational& operator+=(const Rational&);
    constexpr Rational& operator-=(const Rational&);
    constexpr Rational& operator*=(const Rational&);
    constexpr Rational& operator/=(const Rational&);

    // comparison operators
    friend constexpr bool operator==(const Rational&, const Rational&);
    friend constexpr bool operator<(const Rational&, const Rational&);

    // increment and decrement operators
    constexpr Rational operator++(int);
    constexpr Rational operator--(int);
    constexpr Rational& operator++();
    constexpr Rational& operator--();

    // type conversion
    constexpr explicit operator double();

    // stream operators
    friend std::ostream& operator<<(std::ostream&, const Rational&);
    friend std::istream& operator>>(std::istream&, Rational&);

    // arithmetic functions
    constexpr Rational pow(unsigned exp) const;
    constexpr Rational inverse() const;
};

// arithmetic operators
constexpr Rational operator+(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&, const Rational&);
constexpr Rational operator*(const Rational&, const Rational&);
constexpr Rational operator/(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&);
constexpr Rational operator+(const Rational&);


template<typename T>
constexpr Rational::Rational(T p, T q) requires (std::is_integral<T>::value)
    : p{p},
      q{q}
{
    if (q == 0)
        throw std::domain_error{"zero Denominator"};
    simplify();
}

#endif
#include "rational.h"
#include <iostream>
#include <limits>

constexpr Rational operator""_r(unsigned long long p)
{
    // default conversion
    return p;
}

namespace {
    constexpr long long gcd(long long p, long long q)
    {
        return q ? gcd(q, p%q) : p;
    }
}

constexpr Rational::Rational(unsigned long long p, unsigned long long q)
    : Rational{static_cast<long long>(p), static_cast<long long>(q)}
{
    // Retrospectively justify static_cast<> above
    constexpr unsigned long long max_ll = std::numeric_limits<long long>::max();
    if (p > max_ll || q > max_ll)
        throw std::domain_error{"value out of range"};
}

constexpr Rational::Rational(const Rational& rat)
    : Rational{rat.p, rat.q}
{
}

constexpr Rational& Rational::simplify()
{
    // Fix negative denominators
    if (q < 0) {
        p = -p;
        q = -q;
    }

    // Reduce by greatest common divisor
    const auto denom = gcd(p, q);
    p /= denom;
    q /= denom;

    return *this;
}

constexpr Rational& Rational::operator=(const Rational& rat)
{
    p = rat.p;
    q = rat.q;
    return *this;
}

constexpr Rational& Rational::operator+=(const Rational& rat)
{
    p = p * rat.q + q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator-=(const Rational& rat)
{
    p = p * rat.q - q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator*=(const Rational& rat)
{
    p *= rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator/=(const Rational& rat)
{
    if (rat.p == 0)
        throw std::domain_error{"Division by zero not allowed"};
    return *this *= rat.inverse();
}


constexpr Rational operator+(const Rational& a, const Rational& b)
{
    return Rational{a} += b;
}

constexpr Rational operator-(const Rational& a, const Rational& b)
{
    return Rational{a} -= b;
}

constexpr Rational operator*(const Rational& a, const Rational& b)
{
    return Rational{a} *= b;
}

constexpr Rational operator/(const Rational& a, const Rational& b)
{
    return Rational{a} /= b;
}

constexpr Rational operator-(const Rational& r)
{
    return 0 - r;
}

constexpr Rational operator+(const Rational& r)
{
    return r;
}


constexpr bool operator==(const Rational& a, const Rational& b)
{
    return a.p == b.p && a.q == b.q;
}

constexpr bool operator<(const Rational& a, const Rational& b)
{
    return a.p * b.q  <  a.q * b.p;
}


constexpr Rational Rational::operator++(int) // Postfix
{
    Rational temp{*this};
    p += q;
    return temp;
}

constexpr Rational Rational::operator--(int) // Postfix
{
    Rational temp{*this};
    p -= q;
    return temp;
}

constexpr Rational& Rational::operator++()
{
    return *this += 1;
}

constexpr Rational& Rational::operator--()
{
    return *this -= 1;
}

constexpr Rational::operator double()
{
    return static_cast<double>(p) / static_cast<double>(q);
}


std::ostream& operator<<(std::ostream& os, const Rational& rat)
{
    return os << rat.p << ":" << rat.q;
}

std::istream& operator>>(std::istream& is, Rational& rat)
{
    long long p, q;
    char sep;
    if (is >> p >> sep >> q && sep == ':')
        rat = {p, q};
    return is;
}

constexpr Rational Rational::pow(unsigned exp) const
{
    auto x = *this;
    Rational r{1};
    for (;  exp;  exp /= 2) {
        if (exp%2) r *= x;
        x *= x;
    }
    return r;
}

constexpr Rational Rational::inverse() const
{
    return {q, p};
}

Tests

#include "rational.h"
#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);


    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1L + 1/4_r, 5/4_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}

Normally though, we would just let the compiler generate that one:

Rational::Rational(const Rational& rat) = default;
#ifndef _RATIONAL_H_
#define _RATIONAL_H_

#include <iosfwd>
#include <stdexcept>
#include <type_traits>
#include <utility>

class Rational
{
    long long p;
    long long q;

    constexpr Rational& simplify();

public:
    template<typename T> constexpr Rational(T p, T q = 1) requires std::is_integral<T>::value;
    constexpr Rational(unsigned long long p = 0, unsigned long long q = 1);
    constexpr Rational(const Rational&);

    // assignment operators
    constexpr Rational& operator=(const Rational&);
    constexpr Rational& operator+=(const Rational&);
    constexpr Rational& operator-=(const Rational&);
    constexpr Rational& operator*=(const Rational&);
    constexpr Rational& operator/=(const Rational&);

    // comparison operators
    friend constexpr bool operator==(const Rational&, const Rational&);
    friend constexpr bool operator<(const Rational&, const Rational&);

    // increment and decrement operators
    constexpr Rational operator++(int);
    constexpr Rational operator--(int);
    constexpr Rational& operator++();
    constexpr Rational& operator--();

    // type conversion
    constexpr explicit operator double() const;

    // stream operators
    friend std::ostream& operator<<(std::ostream&, const Rational&);
    friend std::istream& operator>>(std::istream&, Rational&);

    // arithmetic functions
    constexpr Rational pow(unsigned exp) const;
    constexpr Rational inverse() const;
};

// arithmetic operators
constexpr Rational operator+(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&, const Rational&);
constexpr Rational operator*(const Rational&, const Rational&);
constexpr Rational operator/(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&);
constexpr Rational operator+(const Rational&);


template<typename T>
constexpr Rational::Rational(T p, T q) requires (std::is_integral<T>::value)
    : p{p},
      q{q}
{
    if (q == 0)
        throw std::domain_error{"zero Denominator"};
    simplify();
}

#endif
#include "rational.h"
#include <iostream>
#include <limits>
    
constexpr Rational operator""_r(unsigned long long p)
{
    // default conversion
    return p;
}
 

namespace {
    constexpr long long gcd(long long p, long long q)
    {
        while (q) {
            p %= q;
            std::swap(p, q);
        }
        return p;
    }
}

constexpr Rational::Rational(unsigned long long p, unsigned long long q)
    : Rational{static_cast<long long>(p), static_cast<long long>(q)}
{
    // Retrospectively justify static_cast<> above
    constexpr unsigned long long max_ll = std::numeric_limits<long long>::max();
    if (p > max_ll || q > max_ll)
        throw std::domain_error{"value out of range"};
}

constexpr Rational::Rational(const Rational& rat) = default;

constexpr Rational& Rational::simplify()
{
    // Fix negative denominators
    if (q < 0) {
        p = -p;
        q = -q;
    }

    // Reduce by greatest common divisor.
    // N.B. if p==0, this results in 0/1 as desired.
    const auto denom = gcd(p, q);
    p /= denom;
    q /= denom;

    return *this;
}

constexpr Rational& Rational::operator=(const Rational& rat)
{
    p = rat.p;
    q = rat.q;
    return *this;
}

constexpr Rational& Rational::operator+=(const Rational& rat)
{
    p = p * rat.q + q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator-=(const Rational& rat)
{
    p = p * rat.q - q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator*=(const Rational& rat)
{
    p *= rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator/=(const Rational& rat)
{
    if (rat.p == 0)
        throw std::domain_error{"Division by zero not allowed"};
    return *this *= rat.inverse();
}


constexpr Rational operator+(const Rational& a, const Rational& b)
{
    return Rational{a} += b;
}

constexpr Rational operator-(const Rational& a, const Rational& b)
{
    return Rational{a} -= b;
}

constexpr Rational operator*(const Rational& a, const Rational& b)
{
    return Rational{a} *= b;
}

constexpr Rational operator/(const Rational& a, const Rational& b)
{
    return Rational{a} /= b;
}

constexpr Rational operator-(const Rational& r)
{
    return 0 - r;
}

constexpr Rational operator+(const Rational& r)
{
    return r;
}


constexpr bool operator==(const Rational& a, const Rational& b)
{
    return a.p == b.p && a.q == b.q;
}

constexpr bool operator<(const Rational& a, const Rational& b)
{
    return a.p * b.q  <  a.q * b.p;
}


constexpr Rational Rational::operator++(int) // Postfix
{
    Rational temp{*this};
    p += q;
    return temp;
}

constexpr Rational Rational::operator--(int) // Postfix
{
    Rational temp{*this};
    p -= q;
    return temp;
}

constexpr Rational& Rational::operator++()
{
    return *this += 1;
}

constexpr Rational& Rational::operator--()
{
    return *this -= 1;
}

constexpr Rational::operator double() const
{
    return static_cast<double>(p) / static_cast<double>(q);
}


std::ostream& operator<<(std::ostream& os, const Rational& rat)
{
    return os << rat.p << ":" << rat.q;
}

std::istream& operator>>(std::istream& is, Rational& rat)
{
    long long p, q;
    char sep;
    if (is >> p >> sep >> q && sep == ':')
        rat = {p, q};
    return is;
}

constexpr Rational Rational::pow(unsigned exp) const
{
    auto x = *this;
    Rational r{1};
    for (;  exp;  exp /= 2) {
        if (exp%2) r *= x;
        x *= x;
    }
    return r;
}

constexpr Rational Rational::inverse() const
{
    if (p < 0) {
        // Make the denominator positive
        return {-q, -p};
    } else {
        return {q, p};
    }
}



//************************************************************************************
// Test Code starts here

#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);

    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1L + 1/4_r, 5/4_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/3_r - 1/3_r, 0_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);
    errors += TEST_EQUAL(1/3_r * 0_r, 0_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);
    errors += TEST_EQUAL((-2/3_r).inverse(), -3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}

Further suggestions

At present, there's no check for overflow in any of the arithmetic operations. This can happen surprisingly quickly in rational arithmetic, so consider how you might detect or avoid it.

Use non-member binary operators
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308

Binary operators should be non-members

We have an asymmetry because we can add Rational + int but not int + Rational. We need a non-member operator+ to get our first argument converted for us.

#ifndef _RATIONAL_H_
#define _RATIONAL_H_

#include <iosfwd>
#include <stdexcept>
#include <type_traits>
#include <utility>

class Rational
{
    long long p;
    long long q;

    constexpr Rational& simplify();

public:
    template<typename T> constexpr Rational(T p, T q = 1) requires std::is_integral<T>::value;
    constexpr Rational(unsigned long long p = 0, unsigned long long q = 1);
    constexpr Rational(const Rational&);

    // assignment operators
    constexpr Rational& operator=(const Rational&);
    constexpr Rational& operator+=(const Rational&);
    constexpr Rational& operator-=(const Rational&);
    constexpr Rational& operator*=(const Rational&);
    constexpr Rational& operator/=(const Rational&);

    // arithmeticcomparison operators
    constexpr Rational operator+(const Rational&) const;
    constexpr Rational operator-(const Rational&) const;
   friend constexpr Rationalbool operator*operator==(const Rational&) const;
    constexpr Rational, operator/(const Rational&) const;
    constexpr Rational operator-() const;
    constexpr Rational operator+() const;
;
    // comparison operators
   friend constexpr bool operator==operator<(const Rational&) const;
    constexpr bool, operator<(const Rational&) const;;

    // increment and decrement operators
    constexpr Rational operator++(int);
    constexpr Rational operator--(int);
    constexpr Rational& operator++();
    constexpr Rational& operator--();

    // type conversion
    constexpr explicit operator double();

    // stream operators
    friend std::ostream& operator<<(std::ostream&, const Rational&);
    friend std::istream& operator>>(std::istream&, Rational&);

    // arithmetic functions
    constexpr Rational pow(unsigned exp) const;
    constexpr Rational inverse() const;
};
 

template<typename// T>arithmetic operators
constexpr Rational::Rational(T p, T q) requires operator+(std::is_integral<T>::value)
    :const p{p}Rational&,
      q{q}
{
    if (q ==const 0Rational&)
        throw std::domain_error{"zero Denominator"};
constexpr Rational operator-(const Rational&, simplify(const Rational&);
}

template<typename T>
constexpr Rational operator+operator*(Tconst aRational&, Rationalconst bRational&)
{
    return Rational{a} + b;
}

template<typename T>;
constexpr Rational operator-/(Tconst aRational&, Rationalconst bRational&)
{;
    returnconstexpr Rational{a} operator- b;
}

template<typename(const T>Rational&);
constexpr Rational operator*operator+(T a, Rationalconst bRational&)
{
    return Rational{a} * b;;
}

template<typename T>
constexpr Rational operator/::Rational(T ap, RationalT bq)
{
 requires (std::is_integral<T>::value)
  return Rational{a} /: b;
p{p}
 ,
template<typename T>
constexpr bool operator==(T a, Rational b)q{q}
{
    returnif Rational{a}(q == b;
}
0)
template<typename T>
constexpr bool operator<(T a, Rational b)
{
    returnthrow Rationalstd::domain_error{a"zero Denominator"};
 < b;  simplify();
}

#endif
#include "rational.h"
#include <iostream>
#include <limits>

constexpr Rational operator""_r(unsigned long long p)
{
    // default conversion
    return p;
}

namespace {
    constexpr long long gcd(long long p, long long q)
    {
        return q ? gcd(q, p%q) : p;
    }
}

constexpr Rational::Rational(unsigned long long p, unsigned long long q)
    : Rational{static_cast<long long>(p), static_cast<long long>(q)}
{
    // Retrospectively justify static_cast<> above
    constexpr unsigned long long max_ll = std::numeric_limits<long long>::max();
    if (p > max_ll || q > max_ll)
        throw std::domain_error{"value out of range"};
}

constexpr Rational::Rational(const Rational& rat)
    : Rational{rat.p, rat.q}
{
}

constexpr Rational& Rational::simplify()
{
    // Fix negative denominators
    if (q < 0) {
        p = -p;
        q = -q;
    }

    // Reduce by greatest common divisor
    const auto denom = gcd(p, q);
    p /= denom;
    q /= denom;

    return *this;
}

constexpr Rational& Rational::operator=(const Rational& rat)
{
    p = rat.p;
    q = rat.q;
    return *this;
}

constexpr Rational& Rational::operator+=(const Rational& rat)
{
    p = p * rat.q + q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator-=(const Rational& rat)
{
    p = p * rat.q - q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator*=(const Rational& rat)
{
    p *= rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator/=(const Rational& rat)
{
    if (rat.p == 0)
        throw std::domain_error{"Division by zero not allowed"};
    return *this *= rat.inverse();
} 


constexpr Rational Rational::operator+(const Rational& rat)a, const Rational& b)
{
    return Rational{*thisa} += rat;b;
}

constexpr Rational Rational::operator-(const Rational& rat)a, const Rational& b)
{
    return Rational{*thisa} -= rat;b;
}

constexpr Rational Rational::operator*(const Rational& rat)a, const
{
    RationalRational& result(*thisb);

    result *= rat;{
    result.simplify();

  return Rational{a} return*= result;b;
}

constexpr Rational Rational::operator/(const Rational& rat)a, const
{
    RationalRational& result(*thisb);
{
    resultreturn Rational{a} /= rat;
    result.simplify();

    return result;b;
}

constexpr Rational Rational::operator-() const Rational& r)
{
    return 0 - *this;r;
}

constexpr Rational Rational::operator+() const Rational& r)
{
    return *this;r;
}


constexpr bool Rational::operator==(const Rational& rat)a, const Rational& b)
{
    return a.p == ratb.p && a.q == ratb.q;
}

constexpr bool Rational::operator<(const Rational& rat)a, const Rational& b)
{
    return a.p * ratb.q  <  a.q * ratb.p;
}


constexpr Rational Rational::operator++(int) // Postfix
{
    Rational temp{*this};
    p += q;
    return temp;
}

constexpr Rational Rational::operator--(int) // Postfix
{
    Rational temp{*this};
    p -= q;
    return temp;
}

constexpr Rational& Rational::operator++()
{
    return *this += 1;
}

constexpr Rational& Rational::operator--()
{
    return *this -= 1;
}

constexpr Rational::operator double()
{
    return static_cast<double>(p) / q;static_cast<double>(q);
}


std::ostream& operator<<(std::ostream& os, const Rational& rat)
{
    return os << rat.p << ":" << rat.q;
}

std::istream& operator>>(std::istream& is, Rational& rat)
{
    long long p, q;
    char sep;
    if (is >> p >> sep >> q && sep == ':')
        rat = {p, q};
    return is;
}

constexpr Rational Rational::pow(unsigned exp) const
{
    auto x = *this;
    Rational r{1};
    for (;  exp;  exp /= 2) {
        if (exp%2) r *= x;
        x *= x;
    }
    return r;
}

constexpr Rational Rational::inverse() const
{
    return {q, p};
}
#include "rational.h"
#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);


    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1L + 1/4_r, 5/4_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}
#ifndef _RATIONAL_H_
#define _RATIONAL_H_

#include <iosfwd>
#include <stdexcept>
#include <type_traits>
#include <utility>

class Rational
{
    long long p;
    long long q;

    constexpr Rational& simplify();

public:
    template<typename T> constexpr Rational(T p, T q = 1) requires std::is_integral<T>::value;
    constexpr Rational(unsigned long long p = 0, unsigned long long q = 1);
    constexpr Rational(const Rational&);

    // assignment operators
    constexpr Rational& operator=(const Rational&);
    constexpr Rational& operator+=(const Rational&);
    constexpr Rational& operator-=(const Rational&);
    constexpr Rational& operator*=(const Rational&);
    constexpr Rational& operator/=(const Rational&);

    // arithmetic operators
    constexpr Rational operator+(const Rational&) const;
    constexpr Rational operator-(const Rational&) const;
    constexpr Rational operator*(const Rational&) const;
    constexpr Rational operator/(const Rational&) const;
    constexpr Rational operator-() const;
    constexpr Rational operator+() const;

    // comparison operators
    constexpr bool operator==(const Rational&) const;
    constexpr bool operator<(const Rational&) const;

    // increment and decrement operators
    constexpr Rational operator++(int);
    constexpr Rational operator--(int);
    constexpr Rational& operator++();
    constexpr Rational& operator--();

    // type conversion
    constexpr explicit operator double();

    // stream operators
    friend std::ostream& operator<<(std::ostream&, const Rational&);
    friend std::istream& operator>>(std::istream&, Rational&);

    // arithmetic functions
    constexpr Rational pow(unsigned exp) const;
    constexpr Rational inverse() const;
};
 

template<typename T>
constexpr Rational::Rational(T p, T q) requires (std::is_integral<T>::value)
    : p{p},
      q{q}
{
    if (q == 0)
        throw std::domain_error{"zero Denominator"};
    simplify();
}

template<typename T>
constexpr Rational operator+(T a, Rational b)
{
    return Rational{a} + b;
}

template<typename T>
constexpr Rational operator-(T a, Rational b)
{
    return Rational{a} - b;
}

template<typename T>
constexpr Rational operator*(T a, Rational b)
{
    return Rational{a} * b;
}

template<typename T>
constexpr Rational operator/(T a, Rational b)
{
    return Rational{a} / b;
}
 
template<typename T>
constexpr bool operator==(T a, Rational b)
{
    return Rational{a} == b;
}

template<typename T>
constexpr bool operator<(T a, Rational b)
{
    return Rational{a} < b;
}

#endif
#include "rational.h"
#include <iostream>
#include <limits>

constexpr Rational operator""_r(unsigned long long p)
{
    // default conversion
    return p;
}

namespace {
    constexpr long long gcd(long long p, long long q)
    {
        return q ? gcd(q, p%q) : p;
    }
}

constexpr Rational::Rational(unsigned long long p, unsigned long long q)
    : Rational{static_cast<long long>(p), static_cast<long long>(q)}
{
    // Retrospectively justify static_cast<> above
    constexpr unsigned long long max_ll = std::numeric_limits<long long>::max();
    if (p > max_ll || q > max_ll)
        throw std::domain_error{"value out of range"};
}

constexpr Rational::Rational(const Rational& rat)
    : Rational{rat.p, rat.q}
{
}

constexpr Rational& Rational::simplify()
{
    // Fix negative denominators
    if (q < 0) {
        p = -p;
        q = -q;
    }

    // Reduce by greatest common divisor
    const auto denom = gcd(p, q);
    p /= denom;
    q /= denom;

    return *this;
}

constexpr Rational& Rational::operator=(const Rational& rat)
{
    p = rat.p;
    q = rat.q;
    return *this;
}

constexpr Rational& Rational::operator+=(const Rational& rat)
{
    p = p * rat.q + q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator-=(const Rational& rat)
{
    p = p * rat.q - q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator*=(const Rational& rat)
{
    p *= rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator/=(const Rational& rat)
{
    if (rat.p == 0)
        throw std::domain_error{"Division by zero not allowed"};
    return *this *= rat.inverse();
}

constexpr Rational Rational::operator+(const Rational& rat) const
{
    return Rational{*this} += rat;
}

constexpr Rational Rational::operator-(const Rational& rat) const
{
    return Rational{*this} -= rat;
}

constexpr Rational Rational::operator*(const Rational& rat) const
{
    Rational result(*this);

    result *= rat;
    result.simplify();

    return result;
}

constexpr Rational Rational::operator/(const Rational& rat) const
{
    Rational result(*this);

    result /= rat;
    result.simplify();

    return result;
}

constexpr Rational Rational::operator-() const
{
    return 0 - *this;
}

constexpr Rational Rational::operator+() const
{
    return *this;
}


constexpr bool Rational::operator==(const Rational& rat) const
{
    return p == rat.p && q == rat.q;
}

constexpr bool Rational::operator<(const Rational& rat) const
{
    return p * rat.q  <  q * rat.p;
}


constexpr Rational Rational::operator++(int) // Postfix
{
    Rational temp{*this};
    p += q;
    return temp;
}

constexpr Rational Rational::operator--(int) // Postfix
{
    Rational temp{*this};
    p -= q;
    return temp;
}

constexpr Rational& Rational::operator++()
{
    return *this += 1;
}

constexpr Rational& Rational::operator--()
{
    return *this -= 1;
}

constexpr Rational::operator double()
{
    return static_cast<double>(p) / q;
}


std::ostream& operator<<(std::ostream& os, const Rational& rat)
{
    return os << rat.p << ":" << rat.q;
}

std::istream& operator>>(std::istream& is, Rational& rat)
{
    long long p, q;
    char sep;
    if (is >> p >> sep >> q && sep == ':')
        rat = {p, q};
    return is;
}

constexpr Rational Rational::pow(unsigned exp) const
{
    auto x = *this;
    Rational r{1};
    for (;  exp;  exp /= 2) {
        if (exp%2) r *= x;
        x *= x;
    }
    return r;
}

constexpr Rational Rational::inverse() const
{
    return {q, p};
}
#include "rational.h"
#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);


    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}

Binary operators should be non-members

We have an asymmetry because we can add Rational + int but not int + Rational. We need a non-member operator+ to get our first argument converted for us.

#ifndef _RATIONAL_H_
#define _RATIONAL_H_

#include <iosfwd>
#include <stdexcept>
#include <type_traits>
#include <utility>

class Rational
{
    long long p;
    long long q;

    constexpr Rational& simplify();

public:
    template<typename T> constexpr Rational(T p, T q = 1) requires std::is_integral<T>::value;
    constexpr Rational(unsigned long long p = 0, unsigned long long q = 1);
    constexpr Rational(const Rational&);

    // assignment operators
    constexpr Rational& operator=(const Rational&);
    constexpr Rational& operator+=(const Rational&);
    constexpr Rational& operator-=(const Rational&);
    constexpr Rational& operator*=(const Rational&);
    constexpr Rational& operator/=(const Rational&);

    // comparison operators
    friend constexpr bool operator==(const Rational&, const Rational&);
    friend constexpr bool operator<(const Rational&, const Rational&);

    // increment and decrement operators
    constexpr Rational operator++(int);
    constexpr Rational operator--(int);
    constexpr Rational& operator++();
    constexpr Rational& operator--();

    // type conversion
    constexpr explicit operator double();

    // stream operators
    friend std::ostream& operator<<(std::ostream&, const Rational&);
    friend std::istream& operator>>(std::istream&, Rational&);

    // arithmetic functions
    constexpr Rational pow(unsigned exp) const;
    constexpr Rational inverse() const;
};

// arithmetic operators
constexpr Rational operator+(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&, const Rational&);
constexpr Rational operator*(const Rational&, const Rational&);
constexpr Rational operator/(const Rational&, const Rational&);
constexpr Rational operator-(const Rational&);
constexpr Rational operator+(const Rational&);


template<typename T>
constexpr Rational::Rational(T p, T q) requires (std::is_integral<T>::value)
    : p{p},
      q{q}
{
    if (q == 0)
        throw std::domain_error{"zero Denominator"};
    simplify();
}

#endif
#include "rational.h"
#include <iostream>
#include <limits>

constexpr Rational operator""_r(unsigned long long p)
{
    // default conversion
    return p;
}

namespace {
    constexpr long long gcd(long long p, long long q)
    {
        return q ? gcd(q, p%q) : p;
    }
}

constexpr Rational::Rational(unsigned long long p, unsigned long long q)
    : Rational{static_cast<long long>(p), static_cast<long long>(q)}
{
    // Retrospectively justify static_cast<> above
    constexpr unsigned long long max_ll = std::numeric_limits<long long>::max();
    if (p > max_ll || q > max_ll)
        throw std::domain_error{"value out of range"};
}

constexpr Rational::Rational(const Rational& rat)
    : Rational{rat.p, rat.q}
{
}

constexpr Rational& Rational::simplify()
{
    // Fix negative denominators
    if (q < 0) {
        p = -p;
        q = -q;
    }

    // Reduce by greatest common divisor
    const auto denom = gcd(p, q);
    p /= denom;
    q /= denom;

    return *this;
}

constexpr Rational& Rational::operator=(const Rational& rat)
{
    p = rat.p;
    q = rat.q;
    return *this;
}

constexpr Rational& Rational::operator+=(const Rational& rat)
{
    p = p * rat.q + q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator-=(const Rational& rat)
{
    p = p * rat.q - q * rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator*=(const Rational& rat)
{
    p *= rat.p;
    q *= rat.q;
    return simplify();
}

constexpr Rational& Rational::operator/=(const Rational& rat)
{
    if (rat.p == 0)
        throw std::domain_error{"Division by zero not allowed"};
    return *this *= rat.inverse();
} 


constexpr Rational operator+(const Rational& a, const Rational& b)
{
    return Rational{a} += b;
}

constexpr Rational operator-(const Rational& a, const Rational& b)
{
    return Rational{a} -= b;
}

constexpr Rational operator*(const Rational& a, const Rational& b)
{
    return Rational{a} *= b;
}

constexpr Rational operator/(const Rational& a, const Rational& b)
{
    return Rational{a} /= b;
}

constexpr Rational operator-(const Rational& r)
{
    return 0 - r;
}

constexpr Rational operator+(const Rational& r)
{
    return r;
}


constexpr bool operator==(const Rational& a, const Rational& b)
{
    return a.p == b.p && a.q == b.q;
}

constexpr bool operator<(const Rational& a, const Rational& b)
{
    return a.p * b.q  <  a.q * b.p;
}


constexpr Rational Rational::operator++(int) // Postfix
{
    Rational temp{*this};
    p += q;
    return temp;
}

constexpr Rational Rational::operator--(int) // Postfix
{
    Rational temp{*this};
    p -= q;
    return temp;
}

constexpr Rational& Rational::operator++()
{
    return *this += 1;
}

constexpr Rational& Rational::operator--()
{
    return *this -= 1;
}

constexpr Rational::operator double()
{
    return static_cast<double>(p) / static_cast<double>(q);
}


std::ostream& operator<<(std::ostream& os, const Rational& rat)
{
    return os << rat.p << ":" << rat.q;
}

std::istream& operator>>(std::istream& is, Rational& rat)
{
    long long p, q;
    char sep;
    if (is >> p >> sep >> q && sep == ':')
        rat = {p, q};
    return is;
}

constexpr Rational Rational::pow(unsigned exp) const
{
    auto x = *this;
    Rational r{1};
    for (;  exp;  exp /= 2) {
        if (exp%2) r *= x;
        x *= x;
    }
    return r;
}

constexpr Rational Rational::inverse() const
{
    return {q, p};
}
#include "rational.h"
#include <sstream>
using namespace std::rel_ops;

int verify(bool result, Rational aval, Rational bval, const char *a, const char *op, const char *b, const char *file, int line)
{
    if (!result)
        std::cerr << file << ":" << line << ": "
                  << a << " " << op << " " << b << "  --  "
                  << aval << " " << op << " " << bval << "\n";
    return !result;
}

template<typename A, typename B>
int verify(A aval, B bval, const char *a, const char *b, const char *file, int line)
{
    if (!(aval == bval))
        std::cerr << file << ":" << line << ": "
                  << a << " == " << b << "  --  "
                  << aval << " == " << bval << "\n";
    return !(aval == bval);
}

#define TEST_OP(a, op, b) verify((a) op (b), (a), (b), #a, #op, #b, __FILE__, __LINE__)
#define TEST_EQUAL(a, b) verify((a), (b), &#a[0], &#b[0], &__FILE__[0], __LINE__)

int main()
{
    int errors{};

    errors += TEST_OP(Rational(1,2), ==, 1_r/2);
    errors += TEST_OP(Rational(1,2), ==, 1/2_r);
    errors += TEST_OP(Rational(1,2), ==, 2/4_r);
    errors += TEST_OP(2/4_r, ==, 1/2_r);
    errors += TEST_OP(-1/2_r, ==, 1/-2_r);
    errors += TEST_OP(2u, ==, 2_r);
    errors += TEST_OP(2_r, ==, 2u);
    errors += TEST_OP(Rational(1,2), !=, 1/3_r);

    errors += TEST_OP(1/3_r, <, 2/5_r);
    errors += TEST_OP(2/5_r, >, 1/3_r);
    errors += TEST_OP(1/3_r, <=, 2/5_r);
    errors += TEST_OP(1/3_r, <=, 1/3_r);


    errors += TEST_EQUAL(1/3_r + 1/4_r, 7/12_r);
    errors += TEST_EQUAL(1L + 1/4_r, 5/4_r);
    errors += TEST_EQUAL(1/4_r - 1/3_r, -1/12_r);
    errors += TEST_EQUAL(1/5_r * 5, 1);
    errors += TEST_EQUAL(-2_r * -2_r, 4);
    errors += TEST_EQUAL(1/5_r / 3, 1/15_r);

    Rational x;
    errors += TEST_EQUAL(x, 0);
    errors += TEST_EQUAL(++x, 1);
    errors += TEST_EQUAL(x++, 1);
    errors += TEST_EQUAL(x, 2);
    errors += TEST_EQUAL(x = 1/2_r, 1/2_r);
    errors += TEST_EQUAL(++x, 3/2_r);
    errors += TEST_EQUAL(++x, 5/2_r);
    errors += TEST_EQUAL(x--, 5/2_r);
    errors += TEST_EQUAL(x--, 3/2_r);
    errors += TEST_EQUAL(x, 1/2_r);

    errors += TEST_EQUAL(Rational(2,3).pow(3), 8/27_r);
    errors += TEST_EQUAL((2/3_r).inverse(), 3/2_r);

    {
        std::stringstream buf;
        Rational r;
        buf << 1/4_r;
        errors += buf.str() != "1:4";
        buf >> r;
        errors += TEST_EQUAL(r, 1/4_r);
    }
    {
        std::stringstream buf("2:5");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 2/5_r);
    }

    {
        std::stringstream buf("2bar");
        Rational r;
        buf >> r;
        errors += TEST_EQUAL(r, 0);
    }

    return errors;
}
Fixed typo, and mention <=>
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308

When we move to C++20, we can use the new "spaceship" operator <=> instead; that simplifies our work even further.

I've made nearly all the changes above (including the changes to the class definition that were out of scope for you) and I'm assuming C++17 with concepts. My ompilationcompilation command is

I've made nearly all the changes above (including the changes to the class definition that were out of scope for you) and I'm assuming C++17 with concepts. My ompilation command is

When we move to C++20, we can use the new "spaceship" operator <=> instead; that simplifies our work even further.

I've made nearly all the changes above (including the changes to the class definition that were out of scope for you) and I'm assuming C++17 with concepts. My compilation command is

Add some more review points and a fully tested example
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308
Loading
I debugged my test - and it was me that was wrong!
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308
Loading
Incorporate suggestions from comments and stuff I thought of overnight
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308
Loading
Source Link
Toby Speight
  • 81.7k
  • 14
  • 101
  • 308
Loading