Overloading Operators and Friends
Overloading Operators
We created a class to declare and define the data
type Fraction.
Nonetheless, our programs still seem a little artificial when using
fractions. For example, when we working with integers, we can have
statements such as
int num1, num2, num3; ... num3 = num1 + num2;
but when working with fraction objects using
our Fraction
class from the last lab, the statements
would be:
Fraction F1, F2, F3; ... F3 = F1.add(F2);
The program would be more readable if we could have used the statement
F3 = F1 + F2;
An operator that operates on two
values is called a binary operator. The binary operator
+
has a
well-defined
meaning (addition) when the two operands (the objects to the
left and right of the operator) are integers, and C++ uses that
meaning. But, when the operator is used with a user-defined class,
the meaning of the operator is unclear to C++. The same is
true of
the other usual binary operators we use with integers and other
built-in types, e.g., -
for subtraction, *
for multiplication, /
for division, &&
for logical
and, ||
for logical or, <
for "less
than", <=
for "less than or equal to", etc.
C++ provides a mechanism for being able to provide a meaning for
all of the above operators for any data type defined by a user using
a class. This mechanism is called operator overloading, since
we are giving an additional meaning to an operator, i.e.,
overloading the meaning of the operator. Once overloaded for a
fraction object, the operator +
has one meaning (the
usual addition of integers) if both the operands are integers, but
has a different meaning (the one supplied in the user-defined class
for fraction objects) when the two operands are fraction objects.
The header and program
files Fraction.h
and Fraction.cc
have the same
class declaration and definition as the last lab, with the addition
of a declaration and definition for an overloaded
operator +
. The program
file UseFract.cc
contains a
program that uses this Fraction
class and demonstrates
the use of the overloaded operator for Fraction
objects. Study the declaration and definition of the overloaded
operator. They are the same as the corresponding declaration and
definition of the member function add
, except that the
name of this member function (the overloaded operator) is
operator +
. Note the usage of the operator in the
program useFract.cc
. Do you expect the values of the
three results to be different?
When the statement
Fraction result1 = f1 + f2;
is executed, the overloaded operator +
is
called; f1
is the implicit parameter to the operator,
and f2 is copied as the value of the formal
parameter pFract
. In general, any overloaded binary
operator in a class has one formal parameter. When using the
overloaded binary
operator, the operand on the
left of the usage of the binary operator is the implicit parameter
in the operator call, and the operand on the right is the actual
parameter.
Note the addition of const
for the
the function add
as well as
the overloaded operator, and their formal parameter. The member
function add
and
the overloaded operator do not change the value of the formal
parameter, nor do they change the value of the implicit parameter;
hence the const
for the functions and their formal
parameters.
As noted above, any of the usual binary operators can be overloaded. The
return type of the operator should be the expected type, i.e., for
addition of fractions, the return type should
be Fraction
, but the return type of the
operator <
should be bool
. Thus, in the
class declaration, the
declaration of the overloaded operator <
will be
bool operator < (const Fraction pFract) const;
How would you write the definition of the operator?
When one defines a class, the
operators =
(assignment) and ==
(equality
check) are both defined for the class. This default =
operator simply assigns the respective
member data objects, using the =
operator for the types
of the member data objects. The default ==
operator
simply compares the respective member data objects using
the ==
operator for the types of the member data
types. This is the reason why, if a class has an array, we cannot
use the =
and ==
operators correctly.
In the program useFract.cc
, when the statement
Fraction result2 = f1 + f2;
the default =
operator is called two times: once to
copy the value of f2
to the formal
parameter pFract
, and then to copy the return
value of the +
operator to the object result2
.
Both these operators can be overloaded, and must be overloaded in a class if the class contains an array.
For the overloaded
operator =
the implicit parameter is the object on the
left of the operator, and the actual parameter is the object on the
right. For example, if the operator =
were overloaded
for the Fraction
class, when the statement
result1 = fraction1;
is executed (where result1
and fraction1
are both of type Fraction
), result1
will
be the implicit parameter to the operator call,
and fraction1
will be the actual parameter. So, should
the declaration of the operator be the following?
void operator = (const Fraction pFract) const;
There are two problems here. Firstly, the implicit parameter will
change so the function should not be
a const
. Secondly, consider the execution of the above
assignment statement using this overloaded
operator. The =
operator will be called to copy the
value of fraction1
to the
object pFract
. This is circular, since we are declaring
the overloaded operator. So, the formal parameter to the overloaded
assignment operator is always passed by reference. Thus, the correct
declaration for the operator is
void operator = (const Fraction & pFract);
Is the return type of void
correct?
Although, the above declaration will not give any errors, and assignment statements like the one above will execute correctly, it will not work like the usual assignment operator where chaining the assignment operator is valid in C++. For example, the statement
int x, y, z; ... x = y = z;
causes x
and y
to get the value
of z
. But, the statement
fraction1 = fraction2 = fraction3;
will not work. This is because C++ treats the above statement as
fraction1 = (fraction2 = fraction3);
Since the return value of our =
operator
is void
, the expression in parentheses does not have
any value. The correct way to declare the operator, so that a
chained assignment statement as above will work is
Fraction & operator = (const Fraction & pFract);
The return type here is a reference to a Fraction
object. Now, in the definition of the operator, besides copying the
member data objects from the formal parameter to the implicit
parameter, the implicit parameter needs to be
returned by reference. The keyword this
in any member
function is a reference to the implicit parameter. So, the return
statement in the assignment operator should be:
return (*this);
The *
before this
is the C++ syntax for
de-referencing a reference, and thus get the object that is being
referenced. So, the return value is the implicit
parameter object, but because of the return type
being Fraction &
the returned value is a reference
to the implicit parameter. Try to write the definition of the
assignment operator.
The equality operator obviously should have a return
type bool
. Neither the implicit parameter object, nor
the formal parameter object will be changed by the operator. So,
what would the header for the operator be?
The implementation of the equality operator will need to compare the individual member data objects using the appropriate equality check for each of their types. Thus, it is useful to define the equality operator for every user-defined type.
If any of the member data objects are arrays, then the equality operator implementation must compare all the individual elements of the arrays for equality.
Overloading Input/Output Operators
C++ allows the input and output operators >>
and <<
, respectively, to be overloaded as well, but
the implementation is a little different than the above operator
overloads.
Consider the input statement
cin >> num;
Clearly, >>
is a binary operator. On the left of
the operator is an input stream, and on the right of the operator is
an object. The type name, given to us by the iostream
library, for input streams is istream
, and the type
name for output streams is ostream
.
Recall that when we overloaded the operator +
and
called it, the operand on the left was passed as the implicit
parameter, and thus, the operator could be a member function. For
the input and output operators, the operand on the left is not of
the type of the user-defined type for which we want to define the
operator. For example, if we were to overload
the >>
operator for the Fraction
class, when we call the operator to output a fraction object, the
operand on the left of the operator would be of
type ostream
, and the
type of the operand on the right of the operator would
be Fraction
. For this reason, overloaded input and
output operators for a class are not member functions of that
class. They should thus be declared in a separate .h
file, and implemented in a corresponding .cc
file.
Also, as we saw with the =
operator above, we would
like to be able to chain the output operator as is legal in
C++. That, is we would like to be able to write the statement
cout << fraction1 << " and " << fraction2 << endl;
In this case, though, C++ treats such a chained statement as if it were parenthesized from the left, i.e., the statement
cout << x << y << endl;
as
((cout << x) << y) << endl;
where each parenthesized statement is an expression whose value is
an output stream. Thus, just as we did in the assignment operator,
the return type of the overloaded <<
operator
should be a reference to the output stream.
Study the header for the overloaded output operator
for Fraction
objects in the
file FractionIO.h
. Since
this is not a member function, we must specify two formal
parameters, one for each of the two operands. When the operator is
used, the operand on the left of the operator is the
actual
parameter corresponding to the first formal parameter, and the
operand on the right of the operator is the actual
parameter corresponding to the second formal parameter. C++ passes
parameters in this way since the function was declared to be an
operator.
Note that the formal parameter stream
is passed by
reference since the stream is changing in the function. Note also
that the object to be output is passed by reference as
a const
so that we do not make a copy of the actual
parameter object
when the operator is called.
Why do we need to include Fraction.h
and
the iostream
library in FractionIO.h
?
Study the definition for the overloaded output operator
for Fraction
objects in the
file FractionIO.cc
. Since
this is not a member function, it does not have access to the
private member data objects of the Fraction
object. So,
it has to use the accessor functions provided by the class.
The operator returns the
parameter stream
, not *stream
as we
did in the assignment operator, since stream
is a
parameter passed by reference, and so is already the object.
Try using the above overloaded output operator in the program
in useFract.cc
.
Friend functions
We saw above that because of the nature of the operands, the input
and output operators for a class cannot be member functions
of that class. At the same time, the input and output
operators, by their very nature, need to access the private
member data objects of the class. C++ provides a mechanism
of friend
functions. We can make the above overloaded
output operator a friend
of the Fraction
class by adding the header for the operator in the class
declaration, and adding the keyword friend
at the
beginning of the header. So, in the class declaration, the header
will be:
friend ostream & operator << (ostream & stream, const Fraction & pFract);
Now, we will not need the file FractionIO.h
. Similarly,
we could add the definition for the operator without the keyword
friend in the
file Fraction.cc
that contains the implementations for
the class functions. This implementation will look exactly as it
does in the file FractionIO.cc
, except in this
implementation, we can access the private member data objects
directly, and thus do not have to call the accessor functions. Now,
we will not need the
file FractionIO.cc
either.
Try including the above output operator as a friend function of
the Fraction
class as described above, and then try
compiling the useFract.cc
program and running it to
make sure that the behaviour is the same as before.