04 - arithmetic
Binary arithmetic operators
These should be very intuitive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
#include <iostream> #include <cassert> // (greatest common divisor) // if you have C++17, you can remove this function and use std::gcd from <numeric> int gcd(int a, int b) { if (b == 0) return a; else return gcd(b, a % b); } class fraction { private: int m_numerator; int m_denominator; static int make_valid_denominator(int value) { if (value == 0) return 1; else return value; } public: fraction(int numerator = 0, int denominator = 1) : m_numerator(numerator) , m_denominator(make_valid_denominator(denominator)) {} void simplify() { const int n = gcd(m_numerator, m_denominator); m_numerator /= n; m_denominator /= n; } int numerator() const { return m_numerator; } int denominator() const { return m_denominator; } }; bool operator==(fraction lhs, fraction rhs) { if (lhs.denominator() == rhs.denominator()) return lhs.numerator() == rhs.numerator(); // a/b == c/d is same as ad/bd == bc/bd // we don't need to compute new denominators, just compare ad and bc return lhs.numerator() * rhs.denominator() == rhs.numerator() * lhs.denominator(); } bool operator!=(fraction lhs, fraction rhs) { return !(lhs == rhs); } fraction operator+(fraction lhs, fraction rhs) { // a/b + c/d = (ad + cb)/bd const int new_numerator = lhs.numerator() * rhs.denominator() + rhs.numerator() * lhs.denominator(); const int new_denominator = lhs.denominator() * rhs.denominator(); fraction result(new_numerator, new_denominator); result.simplify(); return result; } fraction operator-(fraction lhs, fraction rhs) { // a/b - c/d = (ad - cb)/bd const int new_numerator = lhs.numerator() * rhs.denominator() - rhs.numerator() * lhs.denominator(); const int new_denominator = lhs.denominator() * rhs.denominator(); fraction result(new_numerator, new_denominator); result.simplify(); return result; } fraction operator*(fraction lhs, fraction rhs) { // a/b * c/d = ac/bd fraction result(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); result.simplify(); return result; } fraction operator/(fraction lhs, fraction rhs) { assert(rhs != 0); // can't divide by 0 // a/b / c/d = a/b * d/c = ad/bc fraction result(lhs.numerator() * rhs.denominator(), lhs.denominator() * rhs.numerator()); result.simplify(); return result; } fraction operator%(fraction lhs, fraction rhs) { assert(rhs != 0); // can't modulo by 0 // a/b % c/d = (ad % bc)/bd fraction result( (lhs.numerator() * rhs.denominator()) % (rhs.numerator() * lhs.denominator()), lhs.denominator() * rhs.denominator()); result.simplify(); return result; } int main() { assert(fraction(1, 2) + fraction(1, 4) == fraction(3, 4)); assert(fraction(1, 2) - fraction(1, 4) == fraction(1, 4)); assert(fraction(3, 5) * fraction(4, 3) == fraction(4, 5)); assert(fraction(3, 5) / fraction(3, 4) == fraction(4, 5)); assert(fraction(3, 5) * fraction(4, -3) == fraction(-4, 5)); assert(fraction(3, 5) / fraction(3, -4) == fraction(-4, 5)); assert(fraction(2, 1) % fraction(3, 10) == fraction(1, 5)); assert(fraction(2, 1) % fraction(3, -10) == fraction(1, 5)); assert(fraction(2, 1) % fraction(-3, 10) == fraction(1, 5)); assert(fraction(-2, 1) % fraction(3, 10) == fraction(-1, 5)); assert(fraction(2, -1) % fraction(3, 10) == fraction(-1, 5)); } |
There are no special requirements on these operators, but for any typical implementation of overloads:
they return by value
they take parameters by value or const reference (here by value is used because the type is very cheap to copy)
Anything else simply doesn't make any sense.
Unary arithmetic operators
You might already used unary minus in expressions like x = -x
. Unary plus isn't very useful (it does not modify value for built-in integer and floating-point types) but exists for consistency. Unary plus is used in more arcane applications of operator overloading such as EDSLs.
Unary plus and unary minus can be overloaded both as free functions and as member functions. There is no recommendation here - even the standard library is not consistent: std::complex
uses non-member overloads and std::chrono::duration
uses member overloads.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// as member functions (code inside class definition) fraction operator+() const { return *this; } fraction operator-() const { return {-numerator(), denominator()}; } // as free functions fraction operator+(fraction rhs) { return rhs; } fraction operator-(fraction rhs) { return {-rhs.numerator(), rhs.denominator()}; } |
1 2 3 |
assert(fraction(1, 2) == +fraction(1, 2)); assert(fraction(1, 2) == -fraction(-1, 2)); assert(fraction(1, 2) == -fraction(1, -2)); |
Is there any semantic difference between member and non-member unary operator overloads?
Yes. Just like with binary operators, member overloads do not allow implicit convertions for the first argument. Below is an example that allows you to test it - uncomment any of 2 functions and try compiling. I had to create 2 types because built-in types already have these operators defined so the only case where the implicit convertion can be used is between 2 user-defined types.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct bar {}; struct foo { foo() = default; foo(bar) {} // converting constructor // (1) member overload, does not allow implicit convertion // void operator+() const {} }; // (2) non-member overload, allows implicit convertion // void operator+(foo) {} int main() { +bar{}; // works only with (2) } |