CPSC170
Fundamentals of Computer Science II

pre-lab video 1

pre-lab video 2

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.