I created a class for fractions (rational numbers). It contains basic operators for arithmetics, comparisons, and input and output.
I often write classes for fractions, 2D or 3D points, and log probabilities for my projects. I even use templates for underlying types sometimes. For example, 2d points could be based on int, float, or Fraction.
It always takes quite some time to program those classes without bugs. I am creating a class containing all the basic operators that are needed. In the future, I will start by copying that into my project. It will contain
- constructors
- comparison operators
- arithmetic operators
- input and output operators
I think arithmetic operators are the most difficult part. They are often the most complex and I implement both assignment operator+= and usual operator+ operators. I always implement them as member functions and non-member functions respectively. I also often have problems with the input operator >>. Sometimes I use regex for that.
Would you implement the same operators for the class? Would you add or remove anything? Can something be simplified? I would also like to know all C++-related issues. Are there any better or easier ways to do things? I have used the same Makefile for all small C++ scripts I make. I would love to hear opinions about that.
Here is main.cpp:
#include <cassert>
#include <iostream>
#include <compare>
#include <ios>
#include <numeric>
class Fraction {
public:
constexpr Fraction(): Fraction(0) {}
constexpr explicit Fraction(int numerator, int denominator = 1)
: numerator(numerator), denominator(denominator)
{
assert(denominator != 0);
reduce();
}
constexpr bool operator==(const Fraction& other) const = default;
constexpr std::strong_ordering operator<=>(const Fraction& other) const {
return numerator*other.denominator <=> other.numerator*denominator;
}
constexpr Fraction operator-() const { return Fraction { -numerator, denominator }; }
constexpr Fraction& operator+=(const Fraction& other) {
numerator = numerator*other.denominator + other.numerator*denominator;
denominator *= other.denominator;
reduce();
return *this;
}
constexpr Fraction& operator-=(const Fraction& other) {
operator+=(-other);
return *this;
}
constexpr Fraction& operator*=(const Fraction& other) {
numerator *= other.numerator;
denominator *= other.denominator;
reduce();
return *this;
}
constexpr Fraction& operator/=(const Fraction& other) {
operator*=(Fraction { other.denominator, other.numerator });
return *this;
}
friend std::ostream& operator<<(std::ostream& out, const Fraction& fraction);
friend std::istream& operator>>(std::istream& in, Fraction& fraction);
private:
constexpr void reduce() {
int gcd = std::gcd(numerator, denominator);
if (denominator < 0) gcd *= -1;
numerator /= gcd;
denominator /= gcd;
}
int numerator, denominator;
};
constexpr Fraction operator+(Fraction left, const Fraction& right) { return left += right; }
constexpr Fraction operator-(Fraction left, const Fraction& right) { return left -= right; }
constexpr Fraction operator*(Fraction left, const Fraction& right) { return left *= right; }
constexpr Fraction operator/(Fraction left, const Fraction& right) { return left /= right; }
std::ostream& operator<<(std::ostream& out, const Fraction& fraction) {
return out << fraction.numerator << '/' << fraction.denominator;
}
std::istream& operator>>(std::istream& in, Fraction& fraction) {
int denominator, numerator;
char slash;
in >> numerator >> slash >> denominator;
if (slash != '/') {
in.setstate(std::ios_base::failbit);
}
if (in) {
fraction.numerator = numerator;
fraction.denominator = denominator;
}
return in;
}
void ask_fraction() {
Fraction fraction;
std::cout << "Give fraction (for example 2/3): ";
std::cin >> fraction;
if (!std::cin) {
std::cout << "invalid input\n";
std::cin.clear();
return;
}
std::cout << fraction << '\n';
}
int main() {
Fraction x { 4, 6 };
Fraction y { -5, -10 };
Fraction z { -3 };
std::cout << x << '\n';
std::cout << y << '\n';
std::cout << z << "\n\n";
std::cout << "- " << x << " = " << -x << '\n';
std::cout << x << " + " << y << " = " << x+y << '\n';
std::cout << x << " - " << y << " = " << x-y << '\n';
std::cout << x << " * " << y << " = " << x*y << '\n';
std::cout << x << " / " << y << " = " << x/y << "\n\n";
std::cout << std::boolalpha;
std::cout << x << " == " << y << " = " << (x == y) << '\n';
std::cout << x << " != " << y << " = " << (x != y) << '\n';
std::cout << x << " < " << y << " = " << (x < y) << '\n';
std::cout << x << " <= " << y << " = " << (x <= y) << '\n';
std::cout << x << " > " << y << " = " << (x > y) << '\n';
std::cout << x << " >= " << y << " = " << (x >= y) << "\n\n";
ask_fraction();
ask_fraction();
}
Here is Makefile:
appname = fraction
CXX = g++
CXXFLAGS = -Wall -g -std=c++2a
SRCS = main.cpp
.PHONY: test
test: $(appname)
./$(appname) < test_input.txt > user_output.txt
diff -y user_output.txt test_output.txt
.PHONY: run
run: $(appname)
./$(appname)
$(appname): $(SRCS)
$(CXX) $(CXXFLAGS) -o $(appname) $(SRCS)
.PHONY: clean
clean:
$(RM) $(appname) user_output.txt
Here is test_input.txt:
3+2
5/3
Here is test_output.txt:
2/3
1/2
-3/1
- 2/3 = -2/3
2/3 + 1/2 = 7/6
2/3 - 1/2 = 1/6
2/3 * 1/2 = 1/3
2/3 / 1/2 = 4/3
2/3 == 1/2 = false
2/3 != 1/2 = true
2/3 < 1/2 = false
2/3 <= 1/2 = false
2/3 > 1/2 = true
2/3 >= 1/2 = true
Give fraction (for example 2/3): invalid input
Give fraction (for example 2/3): 5/3
0denominator ? Youassert()sure, but what about when the code is not compiled in debug mode ? It might be worth raising a specific exception. \$\endgroup\$rational numbersandc++. In particular, one that I thoroughly reviewed is Implementation of a Rational Number class in C++. \$\endgroup\$