SI204

Class 18: Functions III

Reading

Section 4.1 of Absolute C++

 

Lecture

Pass by Reference rather than Value

Recall that arguments to our functions have been passed by value, meaning that inside the function we get a copy of the argument object given in the function call. Sometimes, however, we'd like to get the actual object from the function call rather than a copy. There are 3 basic reasons for this:

1.      We may want to modify the object.

2.      A "copy" may not make sense for some objects.

3.      If an object is "large", copying may be expensive in terms of time or memory space.

As an example of the first reason: Suppose we have a variable that stands for the hour (with a 24 hour clock), and we go through a simulation that keeps incrementing the hour. A simple ++ won't do, because once we hit 25, the hour goes back to 1. So, we might want a function increaseHour that increments the hour correctly ... which means that the hour variable with which the function gets called should be modified. We might be tempted to say something like this:

void increaseHour(int);
...
void increaseHour(int hourIn)
{
  hourIn++;
  if (hourIn == 25)
    hourIn = 1;
}
    

However, since pass-by-value gives us a copy, nothing really gets changed! We need to pass the hour by reference, which means we get the actual object from the function call, not a copy. This is indicated in C++ by putting an & after the type of the argument.

void increaseHour(int&);
...
void increaseHour(int& hourIn)
{
  hourIn++;
  if (hourIn == 25)
    hourIn = 1;
}
    

Now this does what we want. For example, if this function definition is used in the following code fragment:

int hour = 18;
for(int i = 0; i < 8; i++)
{
  cout << "Hour " << i + 1 
       << " of my 8 hour day starts at "
       << hour << ":00" << endl;
  increaseHour(hour);
}

This fragment will print out something like this:

Hour 1 of my 8 hour day starts at 18:00
Hour 2 of my 8 hour day starts at 19:00
Hour 3 of my 8 hour day starts at 20:00
Hour 4 of my 8 hour day starts at 21:00
Hour 5 of my 8 hour day starts at 22:00
Hour 6 of my 8 hour day starts at 23:00
Hour 7 of my 8 hour day starts at 24:00
Hour 8 of my 8 hour day starts at 1:00

Note that increaseHour(6) will not compile! Why? Well when you pass something like this by reference, that says that it might get modified, and the constant value 6 is not something that can be modified! You need to pass variables, not constants, to functions that take arguments by reference rather than by value.

Power Point Animation of Pass by Reference versus Pass by Value

The World Famous swap

With multi-parameter functions, we really start to see some interesting reasons to use pass-by-reference. For example, one of the most common operations in computing is the swap. Remember when we wanted to start off the GCD algorithm with ints a and b, we wanted to be sure that a was the larger of the two. If we had a function swap that took two ints and swapped their values, we could do the following:

if (b > a)
  swap(a,b);
 
while (b != 0)
{
  r = a % b;
  a = b;
  b = r;
}
cout << a << " is the GCD!" << endl;

The function swap will need to change the values of the variables it's passed, so they must be passed by reference.

void swap(int&, int&);
...
void swap(int& leftIn, int& rightIn)
{
  int temp = leftIn;
  leftIn = rightIn;
  rightIn = temp;
}
 

Using reference to return multiple values

Sometimes there are several things we'd like to return with a function. For example, if you have a program that works with vectors, you might want to convert back and forth from the (r,theta) and (x,y) representation. It'd be nice to have a function that would do this, so we'd give it (r,theta) and it'd give us the appropriate (x,y). Unfortunately, there are two values we'd need to return from our function, and a function always returns at most one thing. What we can do, is pass a variable for x and a variable for y by reference, and modify those variables inside the function so that they contain the proper values.

void polar2rect(double,double,double&,double&);
...
void polar2rect(double r, double theta, double &x, double &y)
{
  x = r*cos(theta);
  y = r*sin(theta);
}
    

With this definition, if I had a vector represented by doubles radius and angle, I could convert it to rectangular coordinates stored in variables x and y by writing:

polar2rect(radius, angle, x, y);

Here is an example of a function that returns a projectile height, range, and flight time in a single function call (projectile).

 

Using reference when copying an object doesn't make sense

Reason 2 from my list of reasons to pass by value is that copying doesn't make sense for some type of objects. Consider the type istream, recalling that both cin and all the ifstream objects we declare are of type istream.

Suppose I have an integer variable T whose value I'd like to be the number of seconds elapsed between two events, and I'd like to read that in our hh:mm:ss format. This is actually a fair bit of work ... not difficult, but time consuming. Let's further suppose that I'll be reading this from both the keyboard (using cin) and from a file (using an ifstream object named filein) at various points in my program. It would be wonderful to have a function readtime that could take either cin or filein as an argument, read the elapsed time in hh:mm:ss format, and return it in seconds. Since both are istream objects, it would be natural to write the function

int readtime(istream);

The problem with this, however, is that it doesn't make sense to copy an input stream object. What would it mean? What would happen, for example, if there was an error while reading the copy ... would the original still be OK? That wouldn't be good. On the other hand, we just have a copy, so it can't affect the original. We have a problem! Copying doesn't make sense. So, we must pass stream objects (both ostream and istream) by reference. With this in mind, we'd define:

int readtime(istream&);
...
int readtime(istream& in)
{
  int h, m, s;
  char c;
  in >> h >> c >> m >> c >> s;
  return h*3600 + m*60 + s;
}

With these definitions, I can say

int k = readtime(cin);

... when I need to read in a time from the keyboard, and I can say:

int m = readtime(filein);

... when I need to read in a time from the file to which filein is attached. Now, my list also mentioned efficiency as a reason to pass by reference rather than pass by value. We haven't seen any explicit examples of "large" objects for which this is important. Our usual types - int, double, char - are all small, so there is no advantage (actually there's a small disadvantage!) to using pass by reference instead of pass by value.

 

Special Note: Why pass filestreams?

Why passing filenames to functions usually is a bad idea

Suppose I want to write a program that reads in two lengths from a file, given in x' y" format, and print out the difference in inches. It should work something like this:

Program Run

data.txt

Enter file name: data.txt
3" difference
3' 2"
2' 11"

As I ponder how to do this, I realize code that reads a length and converts it to inches consists of a couple of lines, and those lines are the same whether I'm reading the first length or the second. So, in top-down design fashion I say to myself "It sure would be nice to have a function readLength that would take care of the reading and converting for me." And so we might arrive at the following proposed solution.

Prototypes

Main

Definitions

int readLength(string fname);
int main()
{
  string DFname;
  cout << "Enter file name: ";
  cin >> DFname;
  int L1 = readLength(name);
  int L2 = readLength(name);
  cout << L1 - L2 << "\" difference"
       << endl;
  return 0;
}
int readLength(string fname)
{
  ifstream fin(fname.c_str());
  int f, i;
  char c;
  fin >> f >> c >> i >> c;
  return 12*f + i;
}

When I run this on the file data.txt from above, the result is 0" difference, which is not what I want. What happened? It is well worth running through this with the debugger and/or with your professor. What you should see is that the first call to readLength creates the variable fin, opens data.txt and attaches it to fin, and reads in the first line, i.e. 3' 2". When the call is over, the stream is closed and fin, being a local variable, is destroyed. then we go through the second call to readLength and ... the exact same thing happens. In other words, readLength creates the variable fin, opens data.txt and attaches it to fin, and reads in the first line, i.e. 3' 2". So both calls read in the first line of the file, the second line is never read, and in main L1 and L2 both have the same value!

The moral of the story is this: passing a file name to a function only makes sense if you want that one function call to completely process the file. If each function call is only going to process a piece of the file, and if a later call should pick up in the file where the earlier call left off, you can't pass the file's name. Instead, you need to pass the file stream to the function.

Passing file streams to functions

So, to get the above scheme to work, main should be the one that creates an ifstream object and attaches it to data.txt, and that ifstream object is what should then be passed to readLength in the two function calls.

Prototypes

Main

Definitions

int readLength(ifstream &fin);
int main()
{
  string name;
  cout << "Enter file name: ";
  cin >> DFname;
  ifstream DFin(name.c_str());
  int L1 = readLength(DFin);
  int L2 = readLength(DFin);
  cout << L1 - L2 << "\" difference"
       << endl;
  return 0;
}
int readLength(ifstream &fin)
{
  int f, i;
  char c;
  fin >> f >> c >> i >> c;
  return 12*f + i;
}

Now we have a working program, one that prints out 3" when we run it on the file data.txt from above, just like it's supposed to. Notice that readLength takes the parameter fin by reference. Why? Well first of all, the whole point is that we have a single filestream and both function calls read from it. If we have multiple filestreams because a pass-by-value call made a copy, we'd be back in the same situation as before: both calls would read the first line of the file. Moreover, making copies of input and output streams simply doesn't make sense: many compilers won't even let you do it. So, input and output streams are always passed by reference!

Why giving your parameters type istream is more flexible than ifstream

Now we have a program that works, and we have this readLength function that is potentially useful in other programs as well. Can we make readLength even more useful than it already is? Well, recall that we discussed that cin has type istream, and that ifstream objects like DFin in the previous program are also of type istream (as well as being of type ifstream). This (and the analogous output situation) is our one and only example of what are called subtypes. How can something be of type istream and type ifstream at the same time? Well, you manage to be of type "college student" and "midshipman" at the same type. It's possible because "midshipman" is a specific kind of "college student". In other words, "midshipman" is a subtype of "college student". The readLength function doesn't use anything specific to ifstream that other istream objects don't also have or do. Therefore, we could simply rewrite readLength so that it takes an istream object.

Prototypes

Main

Definitions

int readLength(istream &in);
int main()
{
  string name;
  cout << "Enter file name: ";
  cin >> DFname;
  ifstream DFin(name.c_str());
  int L1 = readLength(DFin);
  int L2 = readLength(DFin);
  cout << L1 - L2 << "\" difference"
       << endl;
  return 0;
}
int readLength(istream &in)
{
  int f, i;
  char c;
  in >> f >> c >> i >> c;
  return 12*f + i;
}

Everything works just like before. So what's the advantage? Well, since cin is an istream object (though not an ifstream object), we can read a length from the keyboard with readLength by the function call readLength(cin). So now we have a readLength that's even more powerful because it can read from files and from the keyboard.

What's the moral of this story? If you have a function that reads (or writes) information, and if the function doesn't use any ifstream (or ofstream) specific things like .open or .close, define your function to take an argument of type istream (or ostream). This way it can read (or write) to files and to the screen. BTW: remember to pass by reference!

 

Composing Functions

Let's suppose that I had the function max defined as

int max(int a, int b)
{
  if (b > a)
    return b;
  else
    return a;
}
but that my program had three ints, x, y, and z, amongst which I need the largest. If I wrote max(x,y,z) the compiler would complain ... the only max function it knows about only
 takes two arguments! However, I could say the following: max(max(x,y),z).

This is our first example of composition of functions. When the function max() gets called, its two argument expressions are evaluated. The first is max(x,y), which evaluates to the larger of the two values, and the second is simply z. So, what we get out of this is the maximum of all three values.

 

Problems

1.      Incrementing a Military Clock (You might prefer this solution.) This is a simple pass-by-reference example that modifies its argument.

2.      Reading Binary Numbers Here's a simple example in which we use pass-by-reference to avoid making a copy of an istream object.


Assoc Prof Christopher Brown

Last modified by LT M. Johnson 08/16/2007 09:45 AM