Pass by Value vs. Pass by Reference

Pass by value: How it works

Function incHour(): Pass-by-value doesn't work well

Let's take a simple example to see how passing by reference works. 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 incHour 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 incHour(int);

// ...

void incHour(int h)
{
  h++;
  if (h == 25)
    h = 1;
}
However, since pass-by-value gives us a copy, nothing really gets changed! (See diagram above.)

Using pass by reference

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 incHour(int&);

// ...

void incHour(int& h)
{
  h++;
  if (h == 25)
    h = 1;
}
The visualization here is that the parameter h is like a wormhole to the argument incHour was called with. (See diagram above.) 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 = 1; i <= 8; i++)
{
  cout << "Hour " << i << " of my 8 hour day starts at " << hour << ":00" << endl;
  incHour(hour);
}
This example is good for showing how pass-by-reference works, but a lousy example for showing when you should be using it!

In this case, you should write incHour() as a function that takes its parameter with pass-by-value and returns the new time, rather than modifying its argument.

The above code 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

No constant values for passing by reference.

Note that incHour(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.
source codecompiler error

// ex.cpp
 1 #include <iostream>
 2 using namespace std;
 3 
 4 void incHour(int& h); //← ex.cpp:4:6
 5 
 6 int main()
 7 {
 8   incHour(6); //← ex.cpp:8:12
 9   return 0;
10 }
11 // ...
ex.cpp: In function ‘int main()’:
ex.cpp:8:12: error: invalid initialization of non-const 
            reference of type ‘int&’ from an rvalue of type ‘int’
   incHour(6);
            ^
ex.cpp:4:6: note: initializing argument 1 of ‘void incHour(int&)’
void incHour(int& h);
     ^

lvalue vs. rvalue

lvalue

If a function takes a parameter by reference, the technical term for what kind of object can be an argument is lvalue.
An lvalue is anything that can appear on the left-hand side of an assignment.

Q: What can be an lvalue then?

 Answer: variables or objects in an array
We require lvalues for arguments to functions in which the parameter is passed by reference.

rvalue

Another term involved in these kinds of discussions is rvalue, which means anything that can appear on the right-hand side of an assignment — which is a much bigger class of objects.

Deciding whether an expression is an lvalue

Recall that lvalue is something that can appear on the left-hand side (i.e., whose value can be changed by an assignment). So, just see if whether an assignment makes sense when you put it on the left-hand side.

For example, consider the following code:


int b = 1;
int* B = new int[3];


incHour(b);     // OK
incHour(B[0]);  // OK
incHour(b+6);   // not OK
  • Variable b.
     b = 1;
    This makes sense, so b is an lvalue.
  • B[0].
     B[0] = 1;
    This makes sense, so B[0] is an lvalue as well.
  • b+6
     b+6 = 1;
    This doesn't make sense, so b+6 is not an lvalue.

Quick check

Answers:

4 3
4 4
8 4

4 4
5 5
10 10
Give the output of each of the code snippet:

int a = 3;
int b = a;
a++;
cout << a << " " << b << endl;
b++;
cout << a << " " << b << endl;
a = a+b;
cout << a << " " << b << endl;

int a = 3;
int& b = a;
a++;
cout << a << " " << b << endl;
b++;
cout << a << " " << b << endl;
a = a+b;
cout << a << " " << b << endl;

Reason 1: When We Want to Modify the Object

The Famous swap!


int a, b;
cin >> a >> b; 
// assumption: b is larger
for(int i = a; i < b; i++)
  cout << i << ',';
cout << b << endl;
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.

For example, suppose we read two int's in from the user and we want to print out all the integer values from the smaller of the two up to the larger (comma-separate). If the user is kind enough to enter the numbers so that the frst is the smaller, we would write:

But what if we want to allow the user to enter the two numbers in either order, and have the program work regardless?

If we had a function swap that took two ints and swapped their values, we could do the following:


int a, b;
cin >> a >> b;
if (b < a)
  swap(a,b);                // the famous swap!
for(int i = a; i < b; i++)
  cout << i << ',';
cout << b << endl;
... which is a whole lot more convenient than, for example, writing separate for-loops for the a<b case and the b<a case.

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& a, int& b)
{
  int temp = a;  // We need a temporary variable. Why?
  a = b;
  b = temp;
}

Using reference to return multiple values

Sometimes there are several things we'd like to return with a function.

For example, you may want to have a function readfile that works as follows:

  1. Open "data.txt". Let's say the file looks like this:
    N = 4 
    3.1 4.1 5.1 6.1
    
  2. Create an array, read the file data, and finally return the array.
Q: What are the two things you need to return?
Answer:
  1. pointer to the array.
  2. size of the array (i.e., how many objects the array contains).
Therefore, the prototype of the function will look as follows:

double* readfile(int& N); // Note the reference type for N. 
In the main function, you can call the function like:

int N;
double* A = readfile(N);
cout << "data: " << endl;
for(int i=0; i < N; i++)
 cout << A[i] << " ";
Q: Define readfile.
Answer:
double* readfile(int& N) 
{
  ifstream fin("data.txt");

  char c;
  fin >> c >> c;

  fin >> N; // Due to pass-by-reference, N in main() will change too

  double* A = new double[N];
  for(int i=0; i < N; i++)
    fin >> A[i];

  return A;
}

Reason 2: When we pass streams

Assignments for streams are disallowed

Consider the following code:

#include <fstream>
using namespace std;
int main()
{
  ofstream f1("data.txt");
  ofstream f2;
  f2 = f1; // not allowed
  return 0;
}
Q: Compile the code. Does this code compile successfully?
Answer:
No, I see a massive amount of error messages.

You don't need to understand the detail of the messages, but you show know that you cannot do f1=f2.

In fact, when you think about it, it is not clear what it would mean to copy ofstream objects. For example: All these confusions are ruled out by disallowing copying streams.

Pass-by-value for streams is disallowed

For the same reason (pass-by-value involves copying the argument into the parameter), the following code will not compile, either.

void foo(ifstream );

int main()
{
  ifstream fin("data.txt");
  foo(fin);  // pass by value doesn't work 
  //...
}

Passing istream by reference

Copying doesn't make sense for streams. So, we must pass stream objects (both ostream and istream) by reference.

With this in mind, we'd define a function:


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;
}

As you see, the function reads the elapsed amount of time in our hh:mm:ss format.

With these definitions, I can say


int k = readtime(cin);
when I need to read in a time from the keyboard.

Cool stuff

Note: Both cin and all the ifstream objects can be passed as a reference to istream.

As you know, ifstream almost works the same as istream. In fact, ifstream a is sub-type of istream. (There are other sub-types of istream e.g., istringstream).

Therefore, I can also say:


ifstream fin("data.txt");
int m = readtime(fin);  // pass fin as a reference to istream
... when I need to read in a time from a file data.txt using the same function readtime.

It would be wonderful to have a function readtime that can take either cin or fin as an argument, read the elapsed time in hh:mm:ss format, and return it in seconds.

Reason 3: When copying a large object is expensive...

We haven't seen yet 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.

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.