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.