Art Show | Course Home | Homework | Labs | Programming | Schedule & Lecture Notes | Submit

Saint Louis University

Computer Science P125
Introduction to Computer Science

Michael Goldwasser

Spring 2005

Dept. of Math & Computer Science

Lab 10

Overloading Operators


Getting started

To get the needed files for this week's lab, go to a chosen location within your home directory on turing and execute the command:

cp -Rp ~goldwasser/csp125/labs/lab10 .
This will copy our original files for the lab to a subdirectory titled curve in your current working directory.

This includes a text file 'answerForm.txt' which you can use to record your answers to the various questions posed in this lab. Please submit this file of answers electronically to complete the lab.


Overview

This lab is based upon continued exploration of the Rational class. In class, we had intentionally looked at a simplified version of the class in the text. In this lab, we wish to go back and discuss some of the issues that were raised in the text's version of this class.

To start with, open up the file demo.cpp in an editor. This is an example of client code, in that it makes use of the class Rational. Ideally, the class definition should be designed to make things as convenient as possible for such client code.

In the original code, line 15 appears as:

Rational Sum = r.Add(s);
The purpose of this code is to declare a new rational object, Sum and then to set this rational equal to the sum of r and s. This is based upon the Add method which is defined in our Rational class, to produce such a sum.

Notice, however, that the syntax differs greatly from that which we would use for primitive types. For example, if we wished to define a new int which was initialized to the sum of two other integers, we would use a syntax such as:

int z = x + y;

Question 1

Let's see what happens if we try to use such a syntax on a user-defined class such as Rational. Edit line 15 of demo.cpp to instead appear as:
Rational Sum = r + s;
Then recompile the program by typing make demo. Report the exact message given by the compiler in this case.

Revisiting the Previous Lab

It is for good reason that C++ does not automatically evaluate such an operation on a user-defined class. It has no way to be sure of the expected semantics. For example, when performing an addition of Rational objects, such as t = r + s;, the system cannot simply guess the semantics. Though there are two separate numeric data members of the class, namely numerator and denominator, the system would be wrong in assuming that an addition of two rationals should behave as:

t.numerator = r.numerator+s.numerator;
t.denominator = r.denominator + s.denominator;
This is not the proper semantics for rational numbers. That is, (a/b) + (c/d) does not in general equal the result (a+c)/(b+d).

As a second example, the files included in this week's labs also include a version of the BankAccount class from the previous lab. You may try to build this project by typing make banking. If you do so with the original files, you will find a similar syntax error, stemming from line 27 which reads as:

account3 = account1 + account2;
As with the rational class, the immediate complaint of the compiler is that the operator + has no automatic meaning in the context of a user-defined class such as Rational or BankAccount. Again, this is a good thing. In this case, the class is defined with two numeric data members:
double balance;
double interest_rate;

Question 2

Assume that we wished for the semantics of an "addition operation" on two bank accounts, e.g.

account3 = account1 + account2;
to cause a consolidation of those two accounts into a resulting account. What is wrong with an implementation approach based on performing a piecewise addition of each individual data member, as in?
account3.balance = account1.balance+account2.balance;
account3.interest_rate = account1.interest_rate+account2.interest_rate;

Question 3

What effect do you propose such a "consolidation" of two accounts should truly have in this context?


Updating the Rational class definition

Going back to the Rational class, we already have code in the form of the Add method which describes the correct semantics for an addition. The only problem we are facing in the context of this week's lab is that the more natural syntax

Rational Sum = r + s;
is not yet supported. Our first goal will be to update the Rational class definition so as to allow such a syntax.

To do so, we will explore a technique in C++ known as overloading an operator. As we've explored thus far, the system only knows how to perform such operations for built-in data types. Thus it knows that an expression of the form (int + int) results in a third int which represents the sum. Similarly, it knows that (double + double) results in a double. In fact, we have even seen rules for operators which combine mixed types, such as (int + double), which has a resulting value of type double.

If we wish to give some semantics to an expression which involves one or more objects of a user-defined class, we can do so. We simply have to define the behavior. This is called overloading the operator. To fully describe the operation, we must specify four things:

For familiarity, the syntax for describing the desired behavior in C++ is modeled upon the syntax used when defining functions. The general form used is:
return-type operator (operand-list) { body }
Going back to our Rational class as an example, the prototype for the "+" operation could be defined as follows,
Rational operator+(const Rational& r, const Rational& s);
That is, the operator '+' is being defined where the two operands are of type Rational. The result is a Rational as well. Note that the operands are expressed as are parameters to a function. They can be "passed" by value or by reference. Furthermore, you will notice that in this case they are being passed by constant reference, because evaluating the expression (r+s) leaves both r and s themselves unchanged.

Edit the file rational.h and insert the above prototype into that header file. Please place the prototype after the end of the Rational class definition yet before the #endif line. Aside: There are actually two forms for overloading an operator. If defined strictly within a class definition, than the lefthand operator is not explicitly declared. We have chosen to use the form which is strictly outside the class definition, as this is the form used in our text.

Question 4

After adding the prototype to the bottom of the file rational.h, attempt to re-compile the demo (make demo). If you have typed the prototype as expected, the compilation should get farther than before, but still not a complete success. Report the exact message given by the compiler in this case.

At this point, the complaint has changed. With the prototype declared, the compiler is now willing to accept the syntax (r+s) for Rational operands. What is still missing in the big picture is that we have not yet defined a corresponding body to describe the desired behavior. We wish to do this in the file rational.cpp. Start by including the same basic prototype but this time use brackets to denote a body, as in:

Rational operator+(const Rational& r, const Rational& s) {

}
Of course we need to fill in the details. In this case, we do not need to reinvent the wheel. We can rely on the fact that an appropriate Add method is already defined within the class, and so we can express the body by using that method. At this point, you should experiment by trying to complete the task. Keep in mind that the operands are denoted as 'r' and 's' based on the prototype, and that you must use a 'return' statement to report the result of the operation.

Task

Complete the task. If successful, you should rebuild the demo, this type without any compilation errors, and you should be able to run the demo with predictable results. If you are having trouble, ask an instructor for help!

At the end of the lab, we will ask you to submit your updated sourcecode, but not yet (as there will be one more change later).


Overloading the << insertion operator

Much as the original Rational code uses its own Add method to perform additions, it includes an Insert method to print the current value to an output stream. Though functional, this makes the client code a bit cumbersome. For example, the code contains a fragment:

r.Insert(cout);
cout << " + ";
s.Insert(cout);
cout << " = ";
Sum.Insert(cout);
cout << endl;
A simpler way to express this same behavior might be syntax such as:
cout << r << " + " << s << " = " << Sum << endl;

Question 5

Edit demo.cpp and make the above change in syntax. Then attempt to re-compile the demo (make demo). This time, we do not ask you to report the precise error message given by the compiler, but please explain in your own words what you see. Can you explain the essence of the problem?

Let's revisit what we know about the << operator. We have seen it in context when printing various primitive data types. It is a binary operator where the lefthand operand is of type ostream and the righthand operand is a data type such as a char. This allows for code such as

cout << 'h';
In fact, we have also seen that it can be used in a more complicated expression such as
cout << 'h' << 'i';
The above expression is actually a compound expression which is evaluated based on operator precedence as if parenthesized as follows:
(cout << 'h' ) << 'i' ;
The actual signature of the operator in this context is as follows:
ostream& operator<<(ostream& out, const char c);
As we said, the left operand is an ostream reference (typically cout) and the right operand in this case, a character. What is to note is that the return type is itself an ostream. In fact, what the operation does is to add the character to the given stream and then to pass that very same stream as the result (so that it can hence be reused in further operations)

Going back to our Rational class, what we would like to do is to define the behavior for use with insertion onto an ostream. The signature of the function in this case would likely be

ostream& operator<<(ostream& out, const Rational &r);

Task

Add the above prototype to the rational.h file and then add the complete implementation of the operation to the rational.cpp file. Of course, you can rely upon the existing Insert method (just as our overloaded '+' operator depended on the Add method). If successful, you should rebuild the demo, this type without any compilation errors, and you should be able to run the demo with predictable results. If you are having trouble, ask an instructor for help!

When you reach this point, please submit the three files demo.cpp, rational.h and rational.cpp.


More about bank accounts

Let's try to update the BankAccount class definitions to implement a meaningful consolidation semantic for the "+" operator. Though you were allowed to suggest a precise semantic earlier, let me suggest one to use from this point on. If adding account1 and account2, let's create a resulting account which has a balance which equals the sum of the two given balances, and which has a rate which is the lower of the two.

Moreso, we should have the consolidation operation set the balance of the two operand accounts back to zero before proceeding. (after all, we don't want the customer to get credit for the money in the new account while still retaining those balances in the existing accounts).

Based upon this, we suggest the following prototype:

BankAccount operator+(BankAccount& r, BankAccount& s) {

}
Notice that this definition is very similar to the one which we made for the Rational class except that in this case we have declared the operands to be (non-const) references. By doing this, we will be able to modify the state of the operand accounts as well as in defining the resulting account.

Task

As a challenge, make your best attempt at fully implementing the overloaded semantics for '+' in the context of back accounts. As before, you will need to add the prototype into the header file BankAccount.h and will need to give a full implementation of the behavior in the file BankAccount.cpp.

You will not have direct access to the private data members balance and interest_rate. Therefore you will have to use the public accessors and mutators, namely get_balance(), get_rate() and set(double balance, double rate).

If successful, you should rebuild the banking program without any compilation errors, and to run it with predictable results. If you are having trouble, ask an instructor for help!

When you are finished (either because you have succeeded or because time is up), please submit the two files BankAccount.h and BankAccount.cpp.


Michael Goldwasser
CS-P125, Spring 2005
Last modified: Sunday, 03 April 2005
Art Show | Course Home | Homework | Labs | Programming | Schedule & Lecture Notes | Submit