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) |
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); } |
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.
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.
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) |
Binary Operator Member Function | Unary Operator Member Function |
---|---|
(a) | (b) |
this
pointer), and there are no explicit arguments.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); } |
|
(b) | (c) |
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.
a + 5
.this
object, and the right-hand operand, 5, becomes the explicit argument passed inside the parentheses.