21.2 — Overloading the basic arithmetic operators (using non-member functions)

Some of the most commonly used operators in C++ are the arithmetic operators -- that is, the plus operator (+), minus operator (-), multiplication operator (*), and division operator (/). Note that all of the arithmetic operators are binary operators -- meaning they take two operands -- one on each side of the operator. All four of these operators are overloaded in the exact same way.

For binary operators that do not modify their operands, it is preferred to overload the operator using a normal function, and return by value.

To keep things simple, we’ll demonstrate many of our overloaded operators using the following class:

class Cents
{
private:
	int m_cents {};

public:
	explicit Cents(int cents) : m_cents{ cents } { }
	int getCents() const { return m_cents; }
};

Overloading operator+ (using a normal function)

Let’s add an overloaded operator+ that can add two Cents. Here’s what that looks like:

#include <iostream>

class Cents
{
private:
	int m_cents {};

public:
	explicit Cents(int cents) : m_cents{ cents } { }
	int getCents() const { return m_cents; }
};

// Overloaded operator+ to add two Cents (and return a Cents)
Cents operator+(Cents c1, Cents c2)
{
	// We'll use built-in operator+(int, int) to perform integer addition
	// and the Cents constructor to generate a new Cents to return
	return Cents { c1.getCents() + c2.getCents() };
}

int main()
{
	Cents cents1{ 8 };
	Cents cents2{ 6 };
	Cents total { cents1 + cents2 }; // calls operator+(cents1, cents2) and copy constructs result into total
	std::cout << "I have " << total.getCents() << " cents.\n";

	return 0;
}

This produces the result:

I have 14 cents.

Since operator+ is a binary operator that does not modify its operands, we’ll use a normal function, and return by value. The name of the function is the operator’s name: operator+. In this version of operator+, we are going to add two Cents objects together, so our function has two parameters of type Cents (we could also use const Cents&). Finally, the result will be a Cents object, so the return type is Cents.

The implementation is straightforward: we use the getCents() accessor to get the m_cents from each parameter, add them together using built-in operator+, and then return a Cents value with the total. That’s it!

In the body of main(), the expression cents1 + cents2 calls operator+(cents1, cents2). Our overloaded operator+ adds 8 and 6, and then returns a Cents object containing the total (14). This returned object is copy constructed into total. When we print total.getCents(), it prints 14.

Overloading operator-

Let’s overload operator- as well:

#include <iostream>

class Cents
{
private:
	int m_cents {};

public:
	explicit Cents(int cents) : m_cents{ cents } { }
	int getCents() const { return m_cents; }
};

// Overloaded operator+ to add two Cents (and return a Cents)
Cents operator+(Cents c1, Cents c2)
{
	// We'll use built-in operator+(int, int) to perform integer addition
	// and the Cents constructor to generate a new Cents to return
	return Cents { c1.getCents() + c2.getCents() };
}

// Overloaded operator- to subtract two Cents (and return a Cents)
Cents operator-(Cents c1, Cents c2)
{
	// We'll use built-in operator-(int, int) to perform integer subtraction
	// and the Cents constructor to generate a new Cents to return
	return Cents { c1.getCents() - c2.getCents() };
}

int main()
{
	Cents cents1{ 8 };
	Cents cents2{ 6 };
	Cents total { cents1 - cents2 };
	std::cout << "I have " << total.getCents() << " cents.\n";

	return 0;
}

This works analogously to the operator+ case. All we did was change the name of the function to operator- and use built-in operator-.

Overloading operators with operands of different types

When the compiler resolves an expression like x + y into a overloaded operator function call, x becomes the first parameter, and y becomes the second parameter. When x and y have the same type, it does not matter whether you add x + y or y + x -- either way, the same version of operator+ gets called. For example, Cents { 1 } + Cents { 2 } and Cents { 2 } + Cents { 1 } both call operator+(Cents, Cents).

This means that when we write an overloaded binary operator where both operands have the same type, we only need a single overloaded function to handle either ordering of operands.

However, when the operands have different types, x + y does not call the same function as y + x. For example, let’s say we want to be able to add a Cents and an int. If we add Cents { 1 } + 2, this would call operator+(Cents, int). If we add 3 + Cents { 4 }, this would call operator+(int, Cents).

Consequently, when overloading binary operators for operands of different types, we need to write separate overloads for each different ordering of operand types that we want to support.

Here is an example of that:

#include <iostream>

class Cents
{
private:
	int m_cents {};

public:
	explicit Cents(int cents) : m_cents{ cents } { }
	int getCents() const { return m_cents; }
};

Cents operator+(Cents c1, Cents c2)
{
	return Cents { c1.getCents() + c2.getCents() };
}

// Overloaded operator+ to add a Cents and an int
Cents operator+(Cents c1, int val)
{
	return Cents { c1.getCents() + val };
}

// Overloaded operator+ to add an int and a Cents
Cents operator+(int val, const Cents& c1)
{
	return Cents { val + c1.getCents() };
}

int main()
{
	// calls operator+(Cents, int)
	std::cout << "I have " << (Cents { 1 } + 2).getCents() << " cents.\n";

	// calls operator+(int, Cents)
	std::cout << "I have " << (3 + Cents { 4 }).getCents() << " cents.\n";

	return 0;
}

Another example

Let’s do another example. In this example, we’ll define a class named MinMax, which holds a pair of integers that represent a minimum and maximum value.

We’ll also overload operator+ so that we can add two MinMax or a MinMax and an int. The returned MinMax will contain the mininum and maximum values of all the operands. For example, MinMax { 3, 6 } + MinMax { 2, 5 } will return a MinMax { 2, 6 }, and MinMax { 3, 6 } + 7 will return a MinMax { 3, 7 }.

To help out, we’ll use std::min() and std::max() from the <algorithms> header, which respectively return the min and max of their two arguments.

#include <algorithm> // for std::min and std::max
#include <iostream>

class MinMax
{
private:
	int m_min {}; // The min value seen so far
	int m_max {}; // The max value seen so far

public:
	MinMax(int min, int max)
		: m_min { min }, m_max { max }
	{ }

	int getMin() const { return m_min; }
	int getMax() const { return m_max; }
};

MinMax operator+(MinMax m1, MinMax m2)
{
	// Return a new MinMax containing the minimum and maximum of both operands
	return MinMax {
	        std::min(m1.getMin(), m2.getMin()),
        	std::max(m1.getMax(), m2.getMax())
        };
}

MinMax operator+(MinMax m, int val)
{
	return MinMax {
	        std::min(m.getMin(), val),
        	std::max(m.getMax(), val)
        };
}

MinMax operator+(int val, MinMax m)
{
	return MinMax {
	        std::min(m.getMin(), val),
        	std::max(m.getMax(), val)
        };
}

int main()
{
	MinMax m1{ 10, 15 };
	MinMax m2{ 8, 11 };
	MinMax m3{ 3, 12 }; // the minimum of 3 comes from here

	MinMax final{ m1 + m2 + 5 + 8 + m3 + 16 }; // the maximum of 16 comes from here

	std::cout << "Result: (" << final.getMin() << ", " <<
		final.getMax() << ")\n";

	return 0;
}

This program prints:

Result: (3, 16)

which you will note is the minimum and maximum values that we added to final.

Let’s talk a little bit more about how MinMax final { m1 + m2 + 5 + 8 + m3 + 16 } evaluates. Remember that operator+ evaluates from left to right, so m1 + m2 evaluates first. This becomes a call to operator+(m1, m2), which produces the return value MinMax { 8, 15 }. Then MinMax { 8, 15 } + 5 evaluates next. This becomes a call to operator+(MinMax { 8, 15 }, 5), which produces return value MinMax { 5, 15 }. Then MinMax { 5, 15 } + 8 evaluates in the same way to produce MinMax { 5, 15 }. Then MinMax { 5, 15 } + m3 evaluates to produce MinMax { 3, 15 }. And finally, MinMax { 3, 15 } + 16 evaluates to MinMax { 3, 16 }. This final result is then used to initialize final.

In other words, this expression evaluates as MinMax final { (((((m1 + m2) + 5) + 8) + m3) + 16) }, with each successive operation returning a MinMax object that becomes the left-hand operand for the following operator.

Implementing operators using other operators

In the above example, note that operator+(int, MinMax) has the exact same implementation as operator+(MinMax, int). In many cases, it is possible to implement one operator in terms of another operator.

For example, instead of this:

MinMax operator+(MinMax m, int val)
{
	return MinMax {
	        std::min(m.getMin(), val),
        	std::max(m.getMax(), val)
        };
}

MinMax operator+(int val, MinMax m)
{
	return MinMax {
	        std::min(m.getMin(), val),
        	std::max(m.getMax(), val)
        };
}

We can do this:

MinMax operator+(MinMax m, int val)
{
	return MinMax {
	        std::min(m.getMin(), val),
        	std::max(m.getMax(), val)
        };
}

MinMax operator+(int val, MinMax m)
{
	return m + val; // implicitly calls operator+(MinMax, int)
}

This makes our code easier to understand by reducing complexity, and easier to maintain by reducing redundancy. And if the implementation of MinMax ever changes in a way that requires updating the implementation of operator+, we only need to update one function instead of both.

Implementing one operator in terms of another is worth doing when doing so produces simpler code. In cases where the implementation of an overloaded operator is trivial, it may or may not be simpler to implement the operator in terms of another operator.

Best practice

Implement one overloaded operator using another overloaded operator in cases where it reduces complexity.

Calling overloaded operators explicitly

When we use operators in an expression, this results in an implicit invocation of the operator. Implicit invocation can resolve to either a built-in operator or call an overloaded operator.

For example, the expression 2 + 4 implicitly invokes built-in operator+ to add the operands 2 and 4. The overloaded operator+(int, MinMax) function we defined above uses the expression m + val to implicitly invoke overloaded operator+(MinMax, val):

MinMax operator+(int val, MinMax m)
{
	return m + val; // implicitly calls operator+(MinMax, val)
}

Because overloaded operators are implemented as functions, they can also be explicitly (directly) invoked using a function-call syntax. Here’s an equivalent version of the above function that invokes overloaded operator+(MinMax, val) explicitly:

MinMax operator+(int val, MinMax m)
{
	return operator+(m, val); // explicitly calls operator+(MinMax, val)
}

Because they are not functions, built-in operators cannot be called using the function-call syntax.

Key insight

An implicit invocation of an operator (e.g. x + y) can invoke either a built-in or overloaded operator.

An explicit invocation of an operator as a function call (e.g. operator+(x, y)) can only invoke an overloaded operator.

Tip

So when should you use an explicit operator call? There are no conventions around this, but here are a few cases where this can be useful:

  • An explicit operator call makes it clear that an overloaded operator is delegating to another overloaded operator rather than calling a built-in operator.
  • An explicit operator call can be scope qualified (using the scope resolution operator) in order to call an overloaded operator that exists in some other scope. For example, Foo::operator+(x, y) would call the best matching overloaded operator+ that exists in the scope of Foo. An implicit operator call cannot be qualified.

Quiz time

Question #1

Given the following Cents class:

#include <iostream>

class Cents
{
private:
	int m_cents {};

public:
	explicit Cents(int cents) : m_cents{ cents } { }
	int getCents() const { return m_cents; }
};

> Step #1

Add an overloaded operator/(Cents, int). The following program should run:

int main()
{
	Cents cents { 25 };

	std::cout << "How many people are there? ";
	int num {};
	std::cin >> num;

	Cents each { cents / num };
	std::cout << cents.getCents() << " cents divided amongst " << num << " people = " << each.getCents() << " cents per person.\n";

	return 0;
}

And produce the following output:

How many people are there? 4
25 cents divided amongst 4 people = 6 cents per person.

Show Solution

> Step #2

Does it make sense to add an operator/(int, Cents)?

Show Solution

Question #2

> Step #1

Write a class named Fraction that has an integer numerator and denominator member.

The following code should compile:

#include <iostream>

int main()
{
	Fraction f1 { 1, 4 };
	f1.print();
	
	Fraction f2 { 2 };
	f2.print();

	std::cout << f2.getNumerator() << " " << f2.getDenominator() << '\n';

	return 0;
}

This should print:

1/4
2/1
2 1

Show Solution

> Step #2

Our Fraction class has an issue: there are two possible representations for positive and negative fractions. For example, Fraction { -1, -4 } is mathematically equivalent to Fraction { 1, 4 }, but our Fraction class stores it differently. Same with Fraction { -2, 3 } and Fraction { 2, -3 }.

Modify the above program so that the above Fraction pairs are stored identically. The following code should compile:

#include <iostream>

int main()
{
	Fraction f1 { 1, 4 };
	f1.print();
	
	Fraction f2 { -1, -4 };
	f2.print();

	Fraction f3 { -2, 3 };
	f3.print();
	
	Fraction f4 { 2, -3 };
	f4.print();

	return 0;
}

This should print:

1/4
1/4
-2/3
-2/3

Show Hint

Show Hint

Show Solution

> Step #3

Add overloaded multiplication operators to handle multiplication between a Fraction and integer, and between two Fractions.

Hint: To multiply two fractions, first multiply the two numerators together, and then multiply the two denominators together. To multiply a fraction and an integer, multiply the numerator of the fraction by the integer and leave the denominator alone.

The following code should compile:

#include <iostream>

int main()
{
	Fraction f1 {2, 5};
	f1.print();

	Fraction f2 {3, 8};
	f2.print();

	Fraction f3 { f1 * f2 };
	f3.print();

	Fraction f4 { f1 * 2 };
	f4.print();

	Fraction f5 { 2 * f2 };
	f5.print();

	Fraction f6 { Fraction { 1, 2 } * Fraction { 2, 3 } * Fraction { 3, 4 } };
	f6.print();

	Fraction f7 { Fraction { -1, 2 } * Fraction { -2, -3 } };
	f7.print();

	return 0;
}

This should print:

2/5
3/8
6/40
4/5
6/8
6/24
-2/6

Show Solution

> Step #4

The fraction 2/4 is the same as 1/2, but 2/4 is not reduced to the lowest terms. We can reduce any given fraction to lowest terms by finding the greatest common divisor (GCD) between the numerator and denominator, and then dividing both the numerator and denominator by the GCD.

std::gcd() was added to the standard library in C++17 (in the <numeric> header).

If you’re on an older compiler, you can use this function to find the GCD:

#include <cmath> // for std::abs

int gcd(int a, int b)
{
    return (b == 0) ? std::abs(a) : gcd(b, a % b);
}

Modify your code to ensure that Fractions are properly reduced.

The following should compile:

#include <iostream>

int main()
{
	Fraction f1 {2, 5};
	f1.print();

	Fraction f2 {3, 8};
	f2.print();

	Fraction f3 { f1 * f2 };
	f3.print();

	Fraction f4 { f1 * 2 };
	f4.print();

	Fraction f5 { 2 * f2 };
	f5.print();

	Fraction f6 { Fraction { 1, 2 } * Fraction { 2, 3 } * Fraction { 3, 4 } };
	f6.print();

	Fraction f7 { Fraction { -1, 2 } * Fraction { -2, -3 } };
	f7.print();

	Fraction f8 { 0, 6 };
	f8.print();

	return 0;
}

And produce the result:

2/5
3/8
3/20
4/5
3/4
1/4
-1/3
0/1

Show Solution

Question #3

Extra credit: Update the MinMax program in the lesson (the one that prints Result: (3, 16)) so that of the three overloaded operator+ functions, two are implemented using the other.

Show Hint

Show Hint

Show Solution

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
483 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments