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)
}