11.2. Overloaded Operators As Member Functions

Time: 00:04:11 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

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 programs create fraction objects.

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. We need to understand the different ways that programs can instantiate fraction objects, and these ways are affected by the default arguments.
  1. The fraction constructor sets default values for both arguments, allowing three different ways programs can call the constructor, each depending on which defaults it accepts or overrides
  2. Three valid constructor calls:
    1. The program accepts both default values are accepted, creating the fraction 0/1
    2. The program overrides the first default value and accepts the second, converting the integer 5 to the fraction 5/1
    3. The program overrides both default values, creating 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 numeric classes like fraction, the meaning of the arithmetic operators (+, -, *, and /) is generally quite clear. Together, fraction.h and fraction.cpp form a supplier providing 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 but with 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 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 understanding the relationship between function arguments and operator operands. Operands are the values on which operators work or operate, appearing next to the operator. Unary operators precede their operands, and binary operators have operands on their left and right. 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 only use the first notation in practice. The second notation illustrates the relation between the operator notation's operands and the functional notation's 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. The difference between the function defining an overloaded operator and how a program calls it with the operator syntax is a potential source of confusion. In the case of a "regular" function, programmers can see the function's complete name in the call and the correspondence between the arguments and the parameters. In practice, programmers don't use the functional notation when calling an overloaded operator function, but it can help bridge the two notations.
  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 has no arguments. The operand always becomes the implicit argument (bound to the function through the this pointer), and there are no explicit arguments.

Overloading Overloaded Operators

Numerically, adding an integer and fraction is a valid 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, and the fraction addition operator can't complete the operation. Functions, including those implementing 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 the problem by adding a second overloaded addition operator with an integer operand. The new function's left-hand operand is a fraction, and its right-hand operand is an integer:

\[ i = {i \over 1} \text{ so, } {a \over b} = {numerator \over denominator} \text{, and } {c \over d} = {i \over 1} \\[12pt] \] \[ {a \over b} + {c \over d} = {ad + bc \over bd} = {{numerator \times 1} + {denominator \times i} \over {denominator \times 1}} = {{numerator + denominator \times i} \over denominator} \]
(a)
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)'.
(b)(c)
Adding a fraction and an integer. The operation a + 5 is mathematically valid but does not match the Figure 2 addition operator, which "expects" two fraction operands. This example solves the problem with a second overloaded addition operator with an integer operand.
  1. We can express any integer, i, as a fraction by setting its denominator to 1: i/1. Substituting the fraction member names for the variables in the fraction addition formula and simplifying, provides the formula needed to write the second overloaded operator function.
  2. The second overloaded addition operator function matches the signature of the expression (i.e., function call) a + 5.
  3. 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.