11.2. Overloaded Operators As Member Functions

Studying an example is the easiest way of approaching overloaded operators. To demonstrate operators, we revisit the fraction example first introduced in the Classes and Objects chapter. Although the original presentation of the fraction class described how the constructor operates, we'll use fractions throughout this chapter to illustrate different operators and options, so it's appropriate to review how fraction objects are created.

fraction(int n = 0, int d = 1);
 
 
fraction f1;		// i
fraction f2(5);		// ii
fraction f3(2, 3);	// iii
(a)(b)
The fraction constructor with default arguments. It's important for us to understand the different ways that fraction objects may be instantiated, and these ways are affected by the default arguments.
  1. The fraction constructor sets default values for both arguments, so the constructor can be called in different ways depending on which defaults are accepted and which are overridden
  2. The constructor can be called three different ways:
    1. Both default values are accepted, creating the fraction 0/1
    2. The first default value is overridden by the 5 but the second default is accepted, which converts the integer 5 to the fraction 5/1
    3. Both default values are overridden, so the constructor creates the fraction 2/3

Overloading Arithmetic Operators For Numeric Classes

Whenever class designers choose to overload an operator, they are free to choose what an operator means in the context of their class. For example, the + operator may naturally represent a concatenation operation in a string class. For classes like "fraction" that represent some kind of numerical data, the meaning of the arithmetic operators (+, -, *, and /) is generally quite clear. Together, fraction.h and fraction.cpp form a supplier that provides basic fraction operations, implemented as overloaded operators, that any client code may use.

fraction.h
class fraction
{
	private:
		int	numerator;
		int	denominator;

	public:
		fraction(int n = 0, int d = 1) : numerator(n), denominator(d);

		fraction operator+(fraction f);
		fraction operator-(fraction f);
		fraction operator*(fraction f);
		fraction operator/(fraction f);
};
fraction.cpp
#include "fraction.h"

fraction fraction::operator+(fraction f)
{
	int	n = numerator * f.denominator + denominator * f.numerator;
	int	d = denominator * f.denominator;

	return fraction(n, d);
}

fraction fraction::operator-(fraction f)
{
	int	n = numerator * f.denominator - denominator * f.numerator;
	int	d = denominator * f.denominator;

	return fraction(n, d);
}

fraction fraction::operator*(fraction f)
{
	int	n = numerator * f.numerator;
	int	d = denominator * f.denominator;

	return fraction(n, d);
}


fraction fraction::operator/(fraction f)
{
	int	n = numerator * f.denominator;
	int	d = denominator * f.numerator;

	return fraction(n, d);
}
Overloaded operators are implemented with member functions. Converting the fraction class's member functions to overloaded operators only requires changing the function names (highlighted in yellow).

Using Overloaded Operators: Calling The Functions

The above example illustrates that the function prototypes and definitions for overloaded operators are similar to "regular" member functions; they just have different names. But overloaded operators must differ from regular functions somehow or we would just continue to use function names like add and sub. The difference is how we use the operators, that is, how we call the operator functions. The following driver, a client using the fraction supplier files, illustrates how programs call all the overloaded arithmetic operators.

#include "fraction.h"

int main()
{
	fraction	a(3,4);
	fraction	b(1,3);

	fraction	x;
	
	x = a + b;
	x.print();
	x = a - b;
	x.print();
	x = a * b;
	x.print();
	x = a / b;
	x.print();

	return 0;
}
A simple driver to test the overloaded fraction operators. The main function illustrates the typical notation for using overloaded operators.

There it is, there's the big payoff! We can use operators with instances of our classes just like we can use operators with the fundamental data types like int and double. Was it worth it? That's pretty much a matter of opinion. Some computer scientists don't like overloaded operators. The white paper that initially described the purpose and goals of the Java Programming Language makes it clear that the Java designers did not like overloaded operators and deliberately did not borrow them from C++. I personally like overloaded operators, but that preference aside, C++ programmers need to understand them so that they can read and understand programs written by others.

Operands and Arguments: Part 1

Our next challenge is to understand the relationship between function arguments and operator operands. Operands, are the values on which operators work or operate - they appear next to the operator that is operating on them (in the case of binary operators, they appear both to the left and to the right of the operator). Arguments are the data passed from function calls into functions - they are the elements that appear between the opening and closing parentheses that are a part of the function call. Finally, member function arguments may be either implicit or explicit where the implicit argument is the object that calls and is bound to the function through the this pointer, and explicit arguments appear between the parentheses following the function name. All of these concepts come together in overloaded operators implemented with member functions.

The functions that implement overloaded operators may be called in two ways. The first way is based on an operator notation and is common, convenient, and familiar. The second way reflects that overloaded operators are just functions. This notation is uncommon and typically only used to help us understand the transition from "regular" functions and their arguments to overloaded operators and their operands.

fraction x = a + b;
fraction x = a.operator+(b);
(a)(b)
Two ways of calling an overloaded binary operator. Programmers may call overloaded operators with two different notations, but they only use the first notation in practice. The second notation illustrates the relation between the operands appearing in the operator notation and the functional notation arguments.
  1. The common way of calling an overloaded operator
  2. Although the illustrated syntax is valid, it is NOT used in practice as there is no advantage to using operator+ in place of the more common name add used in the previous version
Binary Operator Member Function Unary Operator Member Function
An image showing the expression "a+b" written as the function call "a.operator+(b)" An image showing the expression "-a" written as the function call "a.operator-()"
(a)(b)
The relationship between operands and member-function arguments. Although the functional version, (b), is not used in practice, it does help clarify the relation between the operator's operands and the arguments passed to the implementing function.
  1. Binary operators have two operands. The left hand operand always becomes the implicit argument, while the right hand operator becomes an explicit function argument.
  2. Unary operators have one operand, so the implementing function does not have any arguments. The operand always becomes the implicit argument (bound to the function through the this pointer), and there are no explicit arguments.

Overloaded Asymmetric Member Operators

We'll use two working definitions to help us talk about overloaded operators: symmetric and asymmetric operators. Our definitions are similar to the commutative property defined on real numbers, but they are not the same.

Symmetric operator
type(x) ☺ type(y) ≡ type(y) ☺ type(x)
Asymmetric operator
type(x) ☺ type(y) ≢ type(y) ☺ type(x)
Commutative
a + b ≡ b + a
Not commutative
a - b ≢ b - a
Symmetric and asymmetric operators. In our working definitions, type() returns the data type of an operand and ☺ is an overloaded operator. An operator is symmetric if the left- and right-hand operands are the same type; the operator is asymmetric if the operands are different types.

Numerically, adding an integer to a fraction is a valid commutative operation. Given the fraction 1/2, the operation 1/2 +1 = 1½ = 3/2. Although numerically valid, the operation doesn't match the overloaded operator fraction operator+(fraction f); described above. As currently implemented, the fraction addition operator is symmetric: both operands must be fraction objects. Functions, including those that implement overloaded operators, can be overloaded as long as they obey the requirement that the parameter list of each overloaded version is unique. So, we can solve this problem by adding a second overloaded addition function that has an integer parameter, which is equivalent to adding a second addition operator that has an integer as the right-hand operand:

fraction fraction::operator+(int i)
{
    int n = numerator + denominator * i;

    return fraction(n, denominator);
}
An image showing the expression "a+5" written as the function call "a.operator+(5)"
Adding a fraction and an integer. The expression a + 5 is mathematically valid, and when it is viewed as a function call, it clearly matches the signature of the overloaded operator+ function. Furthermore, as illustrated, the left hand operand, a, becomes the implicit or this object and the right hand operand, 5, becomes the explicit argument passed inside the parentheses. operator+(int) demonstrates an asymmetric operator.