Story
I was trying to solve a hard problem from Project Euler earlier today (continued from yesterday) and I wanted to graph some things up and play a little bit with fractions. To work with fractions, I decided to implement a little OOP-ish fraction class. I was hoping to brush up whatever little knowledge of C++ I have into trying and implementing a full blown templated project (as I don't practice software design very often and am relatively on the newer side of programming (1.8-ish years)). After fixing what seems like a million template errors (we all can agree on how terrible compilers are with reporting template errors), the following code is what I've arrived at in one day's work.
Code
/**
Lost Arrow (Aryan V S)
Monday 2020-10-19
**/
#ifndef FRACTION_H_INCLUDED
#define FRACTION_H_INCLUDED
#include <algorithm>
#include <stdexcept>
#include <type_traits>
#include <utility>
/**
* The fraction class is meant to represent and allow usage of fractions from math.
* It is represented by a numerator and a denominator (private context).
* The interface provides you with simple operations:
* - Construction/Assignment
* - Comparison (equality/inequality, less/greater than, lesser/greater equal to, three-way (C++20 spaceship operator))
* - Arithmetic (addition/subtraction, multiplication/division)
* - Conversion (bool, decimal)
* - I/O Utility
* - Access/Modification
*/
template <typename T>
class fraction {
/**
* fraction <T> is implemented to work only with T = [integer types]
* using floating point or any other types in fractions doesn't make sense.
*/
static_assert(std::is_integral <T>::value, "fraction requires integral types");
public:
/**
* Constructors
*/
fraction ();
fraction (const T&, const T&);
fraction (T&&, T&&);
fraction (const fraction&);
fraction (fraction&&);
fraction (const std::pair <T, T>&);
fraction (std::pair <T, T>&&);
/**
* Assignment
*/
fraction <T>& operator = (const fraction&);
fraction <T>& operator = (fraction&&);
/**
* Access/Modification
*/
T nr () const;
T dr () const;
T& nr ();
T& dr ();
std::pair <T, T> value () const;
std::pair <T&, T&> value ();
/**
* Utility
*/
void print (std::ostream& = std::cout) const;
void read (std::istream& = std::cin);
template <typename S>
S decimal_value () const;
std::pair <T, T> operator () () const;
std::pair <T&, T&> operator () ();
operator bool() const;
/**
* Comparison
*/
int8_t spaceship_operator (const fraction <T>&) const;
template <typename S>
friend bool operator == (const fraction <S>&, const fraction <S>&);
template <typename S>
friend bool operator != (const fraction <S>&, const fraction <S>&);
template <typename S>
friend bool operator <= (const fraction <S>&, const fraction <S>&);
template <typename S>
friend bool operator >= (const fraction <S>&, const fraction <S>&);
template <typename S>
friend bool operator < (const fraction <S>&, const fraction <S>&);
template <typename S>
friend bool operator > (const fraction <S>&, const fraction <S>&);
/**
* Arithmetic
*/
fraction <T>& operator += (const fraction <T>&);
fraction <T>& operator -= (const fraction <T>&);
fraction <T>& operator *= (const fraction <T>&);
fraction <T>& operator /= (const fraction <T>&);
private:
mutable T m_nr, m_dr;
/**
* Utility
*/
void normalize () const;
};
/** ::normalize
* The normalize utility function converts a fraction into its mathematically equivalent
* "simplest" form.
* The functions throws a domain error if the denominator is ever set to 0.
*/
template <typename T>
inline void fraction <T>::normalize () const {
if (m_dr == 0)
throw std::domain_error("denominator must not be zero");
T gcd = std::gcd(nr(), dr());
m_nr /= gcd;
m_dr /= gcd;
}
/** ::spaceship_operator
* The spaceship operator provides similar functionality as the <=> operator introduced in C++20.
* If self < other : return value is -1
* If self == other : return value is 0
* If self > other : return value is 1
*/
template <typename T>
inline int8_t fraction <T>::spaceship_operator (const fraction <T>& other) const {
normalize();
other.normalize();
if ((*this)() == other())
return 0;
if (nr() * other.dr() < other.nr() * dr())
return -1;
return 1;
}
/**
* Constructors
*/
template <typename T>
fraction <T>::fraction ()
: m_nr (0)
, m_dr (1)
{ }
template <typename T>
fraction <T>::fraction (const T& nr, const T& dr)
: m_nr (nr)
, m_dr (dr)
{ normalize(); }
template <typename T>
fraction <T>::fraction (T&& nr, T&& dr)
: m_nr (std::move(nr))
, m_dr (std::move(dr))
{ normalize(); }
template <typename T>
fraction <T>::fraction (const fraction <T>& other)
: fraction (other.nr(), other.dr())
{ }
template <typename T>
fraction <T>::fraction (fraction <T>&& other)
: fraction (std::move(other.nr()), std::move(other.dr()))
{ }
template <typename T>
fraction <T>::fraction (const std::pair <T, T>& other)
: fraction (other.first, other.second)
{ }
template <typename T>
fraction <T>::fraction (std::pair <T, T>&& other)
: fraction (std::move(other.first), std::move(other.second))
{ }
/**
* Assignment
*/
template <typename T>
fraction <T>& fraction <T>::operator = (const fraction <T>& other) {
if (this != &other) {
nr() = other.nr();
dr() = other.dr();
}
return *this;
}
template <typename T>
fraction <T>& fraction <T>::operator = (fraction <T>&& other) {
if (this != &other) {
nr() = std::move(other.nr());
dr() = std::move(other.dr());
}
return *this;
}
/** ::nr
* An accessor function that returns a copy of the numerator value to the caller.
*/
template <typename T>
inline T fraction <T>::nr () const {
return m_nr;
}
/** ::dr
* An accessor function that returns a copy of the denominator value to the caller.
*/
template <typename T>
inline T fraction <T>::dr () const {
return m_dr;
}
/** ::nr
* An modification function that returns a reference of the numerator value to the caller.
*/
template <typename T>
inline T& fraction <T>::nr () {
return m_nr;
}
/** ::dr
* An modification function that returns a reference of the denominator value to the caller.
*/
template <typename T>
inline T& fraction <T>::dr () {
return m_dr;
}
/** ::print
* An utility function that prints to the standard output stream parameter in the form: nr/dr
*/
template <typename T>
inline void fraction <T>::print (std::ostream& stream) const {
normalize();
stream << nr() << "/" << dr();
}
/** ::read
* An utility function that reads from the standard input stream in the form: nr dr
*/
template <typename T>
inline void fraction <T>::read (std::istream& stream) {
stream >> nr() >> dr();
normalize();
}
/** ::decimal_value
* An utility function that converts a fraction to its mathematically equivalent floating point representation
* This function requires its template type as a floating point type.
*/
template <typename T>
template <typename S>
inline S fraction <T>::decimal_value () const {
static_assert(std::is_floating_point <S>::value, "decimal notation requires floating point type");
normalize();
return static_cast <S> (nr()) / static_cast <S> (dr());
}
/** ::value
* An utility function that returns a standard pair with the copy of the numerator and denominator to the caller.
*/
template <typename T>
inline std::pair <T, T> fraction <T>::value () const {
return std::pair <T, T> (nr(), dr());
}
/** ::value
* An utility function that returns a standard pair with a refernce to the numerator and denominator to the caller.
*/
template <typename T>
inline std::pair <T&, T&> fraction <T>::value () {
return std::pair <T&, T&> (nr(), dr());
}
/** ::operator ()
* Provides same functionality as ::value with copy.
*/
template <typename T>
inline std::pair <T, T> fraction <T>::operator () () const {
return value();
}
/** ::operator ()
* Provides same functionality as ::value with reference.
*/
template <typename T>
inline std::pair <T&, T&> fraction <T>::operator () () {
return std::pair <T&, T&> (nr(), dr());
}
/** ::bool()
* Converts a fraction to a boolean value which is false if the numerator is 0.
*/
template <typename T>
inline fraction <T>::operator bool() const {
return bool(nr());
}
/**
* Comparison (implemented with the spaceship_operator
*/
template <typename S>
inline bool operator == (const fraction <S>& left, const fraction <S>& right) {
return left.spaceship_operator(right) == 0;
}
template <typename S>
inline bool operator != (const fraction <S>& left, const fraction <S>& right) {
return left.spaceship_operator(right) != 0;
}
template <typename S>
inline bool operator <= (const fraction <S>& left, const fraction <S>& right) {
return left.spaceship_operator(right) <= 0;
}
template <typename S>
inline bool operator >= (const fraction <S>& left, const fraction <S>& right) {
return left.spaceship_operator(right) >= 0;
}
template <typename S>
inline bool operator < (const fraction <S>& left, const fraction <S>& right) {
return left.spaceship_operator(right) == -1;
}
template <typename S>
inline bool operator > (const fraction <S>& left, const fraction <S>& right) {
return left.spaceship_operator(right) == 1;
}
/**
* Arithmetic
*/
template <typename T>
fraction <T>& fraction <T>::operator += (const fraction <T>& other) {
normalize();
other.normalize();
T lcm = std::lcm(dr(), other.dr());
nr() = nr() * (lcm / dr()) + other.nr() * (lcm / other.dr());
dr() = lcm;
normalize();
return *this;
}
template <typename T>
fraction <T>& fraction <T>::operator -= (const fraction <T>& other) {
return *this += fraction <T> (-other.nr(), other.dr());
}
template <typename T>
fraction <T>& fraction <T>::operator *= (const fraction <T>& other) {
normalize();
other.normalize();
nr() *= other.nr();
dr() *= other.dr();
normalize();
return *this;
}
template <typename T>
fraction <T>& fraction <T>::operator /= (const fraction <T>& other) {
return *this *= fraction <T> (other.dr(), other.nr());
}
template <typename T>
fraction <T> operator + (fraction <T>& left, fraction <T>& right) {
fraction <T> result (left);
return result += right;
}
template <typename T>
fraction <T> operator - (fraction <T>& left, fraction <T>& right) {
fraction <T> result (left);
return result -= right;
}
template <typename T>
fraction <T> operator * (fraction <T>& left, fraction <T>& right) {
fraction <T> result (left);
return result *= right;
}
template <typename T>
fraction <T> operator / (fraction <T>& left, fraction <T>& right) {
fraction <T> result (left);
return result /= right;
}
#endif // FRACTION_H_INCLUDED
Questions and other stuff
According to modern programming standards in C++, how well is my code written? According to software engineering standards, what are the areas I need to improve in? Is my code efficient and well written? Are there any ways I could speed up some computations for efficiency? What improvements to the interface are possible? What could I clean up or refactor in my code? Other relevant insights and tips/discussion/reviews will be appreciated.
To make my implementation more efficient, I'm thinking that I have normalize()
at a lot of places which runs in O(log(a.b))
and I could add another private variable which checks if a fraction has been normalized or not. This could get rid of redundant runs on normalize. Any other suggestions will be warmly welcomed.
Please don't mind the terrible documentation. I would really do that part much better if my goal was to write a fraction implementation for the sake of writing a fraction implementation and not for solving some problem, and if I were to not have to finish it in one days work. Also, any comments on a good procedure to document stuff are welcome (I know this is subjective and different people have different answers to this but something like a short 101 will likely provide helpful info for future projects).
Thanks SE Community!