A parameter passing mechanism is a mechanism or facility which is provided by a programming language to pass data (i.e. parameters) between a caller and a callee.

This writeup will explore most of the mechanisms which have been provided by programming languages over the years to pass parameters between routines (feel free to suggest "worthy" mechanisms which are missing). Specifically, the following mechanisms will be covered:

This writeup is also an attempt to bring the discussion of all of these different parameter passing mechanisms under one roof.

YAIL

An example program that illustrates each mechanism will be provided. The example programs will be written in an imaginary block structured programming language called YAIL (an acronym formed by Yet Another Imaginary Language).

Here's a short YAIL example to give you the flavour of the language:

 1  begin
 2     integer a, b, c;
 3     procedure doit(integer value w)
 4        begin
 5           integer b;
 6           a = 5;
 7           b = 10;
 8           c = a * b;
 9           write("a is ",a);
10           write("b is ",b);
11           write("c is ",c);
12           write("w is ",w);
13        end;
14     a = 6;
15     b = 7;
16     c = a * b;
17     doit(c);
18     write("a is now ",a);
19     write("b is now ",b);
20     write("c is now ",c);
21  end.
The output of this program would be
a is 5
b is 10
c is 15
w is 42
a is now 5
b is now 7
c is now 15
Although, I trust, this example is pretty straightforward, there are a couple of points that I'd like to make:
  • as YAIL supports a wide variety of parameter passing mechanisms, it's necessary to specify which mechanism is to be used for each parameter. For example, the value keyword on line 3 requests that the w parameter be passed by value.

  • the declaration of b on line 5 masks or hides the global variable b that was declared on line 2. i.e. within the doit procedure, references to b refer to the local variable b declared on line 5, not the global variable b declared on line 2. On the other hand, references to a and c within doit refer to the global variables a and c declared on line 2.

  • the features of YAIL have been selected to allow me to demonstrate the various parameter passing mechanisms. I didn't use a mainstream programming language like C or Java as none of the mainstream languages support all of the mechanisms which will be discussed here.

  • Example programs within this writeup will have line numbers provided when the example is long enough to warrant them and the associated commentary requires them.

Definitions

In order to avoid confusion and a lot of extra words (i.e. typing), I'd like to define two terms - argument and parameter. At least within the context of this writeup, an argument is what is passed to a routine. For example, c is the first and only argument which is passed to doit on line 17 in the above example. At least within the context of this writeup, a parameter is the variable within the procedure that the argument is passed to. For example, ww on line 3 is the parameter that c is passed to by the call to doit on line 17. Other names for these terms have been used over the years (e.g. actual parameter vs formal parameter). I prefer argument and parameter as they strike me as less ambiguous than using an adjective applied to the word parameter to distinguish between the two concepts.

We need another pair of terms which I intend to borrow from the C programming language - an lvalue is something which can appear on the left hand side of an assignment statement (i.e. it is something that can be assigned to). An rvalue is something which may appear on the right hand side of an assignment statement (i.e. it's a value which might (e.g. a) or might not (e.g. a * b) also be an lvalue).

In the above example, a and w are some of the lvalues and a, w, 5 and a*b are some of the rvalues.


Reach out and touch

What is almost certainly the oldest parameter passing mechanism involves putting the arguments into particular variables and having the procedure access its parameters from the same variables. Here's a short example:
begin
   integer p1, p2, rval;
   procedure reach()
      begin
      rval = p1 * p2;
      end;
   p1 = 10;
   p2 = 5;
   reach();
   write("first product is ",rval);
   p1 = 20;
   p2 = 10;
   reach();
   write("second product is ",rval);
end.
which produces
first product is 50
second product is 200
No doubt about it - this is pretty ugly.

The FORTRAN language's COMMON block facility is a variation on this mechanism. One could argue that the fairly common practice of leaving values in instance variables and then calling from one method to another within a class in an object-oriented language like Java is also somewhat equivalent to this method although it is cleaner as the scope of an instance variable is fairly small.

Let's just move on shall we?

Call by value

The initial value of a call by value parameter is the value of the corresponding argument (i.e. the value of the argument is copied into the parameter as part of calling the procedure). As this mechanism is very common, a short example should suffice:
begin
   integer a;
   procedure byvalue(integer value w)
      begin
      write("w is ",w);
      w = w * 3;
      write("a is now ",a);
      write("w tripled is ",w);
      end;
   a = 2;
   byvalue(a);
   write("a is currently ",a);
   byvalue(a*5);
   write("a's final value is ",a);
end.
The output from this program would be
w is 2
a is now 2
w tripled is 6
a is currently 2
w is 10
a is now 2
w tripled is 30
a's final value is 2
Although obvious to those familiar with call by value (i.e. probably everyone reading this writeup), I'd like to emphasize a few points in order to be able to refer back to them in the following sections:
  • The assignment to w within byvalue doesn't affect the value of a.

  • The fact that the value of the argument is copied into w means that the argument can be an expression (i.e. an rvalue) as illustrated by the second call to byvalue)
The call by value mechanism is used by languages like C and Java.

Call by result

A call by result parameter's initial value is undefined. When the procedure is finished, the final value of a call by result parameter is copied out to the argument (i.e. the argument gets the resulting value of the parameter as computed by the procedure). Here's an example:
begin
   integer a, b;
   procedure byresult(integer value t, integer result w)
      begin
      write("t is ",t);
      // we can't print out the value of w here
      // as it is "undefined".
      w = t * 3;
      write("a is now ",a);
      end;
   a = 10;
   write("a is ",a);
   byresult(5,a);
   write("a is currently ",a);
   byresult(a,a);
   write("a's final value is ",a);
end.
The output from this example would be
a is 10
t is 5
a is now 5
a is currently 15
t is 15
a is now 15
a's final value is 45
A few notes are definitely in order:
  • changing the value of w while inside byresult has no effect on the value of the corresponding argument (i.e. a).

  • since the final value of w is passed back out to the corresponding argument, the corresponding argument must be something which can be assigned to (i.e. an lvalue).
Call by result was implemented by a variety of early programming including Algol W. On a more modern note, Microsoft's C# programming language supports "out" parameters which provide call by result functionality.

The second call to byresult illustrates one way to get an argument's value into a procedure and get a resulting value back into the same identifier. There's a much better way to do this which is the mechanism which we'll discuss next.

Call by value result

The call by value result mechanism is really just call by value combined with call by result. A call by value result parameter's initial value is the value of the corresponding argument and the final value of the parameter is copied out to the argument. Let's look at an example:
begin
   integer a, b;
   procedure byvalueresult(integer value result w)
      begin
      write("w is ",w);
      w = w * 3;
      write("a is now ",a);
      end;
   a = 10;
   write("a is ",a);
   byvalueresult(a);
   write("a's final value is ",a);
end.
The output of this program would be:
a is 10
w is 10
a is now 10
a's final value is 30
A few notes are in order:
  • changing the value of w while inside byvalueresult has no impact on the value of the corresponding argument (i.e. a)
  • the argument for a call by value result parameter must be an lvalue
I'm not aware of any modern programming languages which are defined to provide a call by value result mechanism. My recollection is that recent versions of the FORTRAN programming language specification allow an implementation (i.e. a FORTRAN compiler) to use call by value result or call by reference to pass parameters (i.e. a FORTRAN programmer is not supposed to write code which behaves differently depending on whether a particular implementation uses call by value result or call by reference).

P.S. I'm told that you young'ns out there have been taught to use the term call by value return when referring to call by value result. That's ok I suppose. If I can get used to airplane instead of aeroplane then I suppose that I can eventually get used to call by value return instead of call by value result (grin).

Call by reference

The call by reference mechanism is fundamentally different than the mechanisms discussed above. A call by reference parameter is a reference to the argument. As such, any use within a procedure of a call by reference parameter is actually a use of the argument. The name of this mechanism, call by reference, is a reflection of how the word reference is usually used instead of use. e.g. any reference within a procedure to a call by reference parameter is actually a reference to the argument.

Let's look at an example:

begin
   integer a, i;
   integer v[10];
   procedure byreference(integer reference w)
      begin
      i = i + 1;
      write("i is now ",i);
      write("w is ",w);
      w = w * 3;
      write("a is now ",a);
      write("v[1] is now ",v[1]);
      write("v[2] is now ",v[2]);
      end;
   a = 10;
   i = 1;
   v[1] = 10;
   v[2] = 20;
   write("a is ",a);
   byreference(a);
   write("a's final value is ",a);
   write("v[1] is ",v[1]);
   write("v[2] is ",v[2]);
   i = 1;
   byreference(v[i]);
   write("v[1]'s final value is ",v[1]);
   write("v[2]'s final value is ",v[2]);
end.
The output of this program would be:
a is 10
i is now 2
w is 10
a is now 30
v[1] is now 10
v[2] is now 20
a's final value is 30
v[1] is 10
v[2] is 20
i is now 2
w is 10
a is now 30
v[1] is now 30
v[2] is now 20
v[1]'s final value is 30
v[2]'s final value is 20
A few notes are (possibly) in order:
  • assigning to w while inside byreference immediately changes the value of the corresponding argument (i.e. a)
  • the argument for a call by reference parameter must be an lvalue
  • just ignore the part dealing with the v array for now (it's purpose will become clear shortly)
Optimizing compiler writers don't much like call by reference as it can be very difficult to determine what impact a change to a global variable might have on call by reference parameters and vice-versa. For example, the compiler may have to assume that any assignment to a global variable could have modified the argument associated with any call by reference parameter whose type is the same as the global variable. Similarily, it may have to assume that any assignment to a call by reference parameter could have modified any global variable of the same type as the parameter in addition to possibly modifying the argument associated with any other call by reference parameter of the same type.

The earliest versions of FORTRAN required that parameters be passed by reference. Although not certain, I believe that the difficulty of optimizing such programs has led more recent versions of the FORTRAN specification to allow a compiler to use either call by value result or call by reference.

Call by name

The call by name mechanism is also fundamentally different than any of the mechanisms described above. A call by name parameter is an alias or alternative name for the corresponding argument. What this means is that any use of a call by name parameter is actually a use of the corresponding argument as it is expressed in the function call.

Consider the following program:

 1  begin
 2     integer a, i;
 3     integer v[10];
 4     procedure byname(integer name w)
 5        begin
 6        write("i is ",i);
 7        write("w is ",w);
 8        i = i + 1;
 9        write("i is now ",i);
10        write("w is now ",w);
11        w = w * 3;
12        write("a is now ",a);
13        write("v[1] is now ",v[1]);
14        write("v[2] is now ",v[2]);
15        end;
16     a = 10;
17     i = 1;
18     v[1] = 10;
19     v[2] = 20;
20     write("a is ",a);
21     byname(a);
22     write("a's final value is ",a);
23     write("v[1] is ",v[1]);
24     write("v[2] is ",v[2]);
25     i = 1;
26     byname(v[i]);
27     write("v[1]'s final value is ",v[1]);
28     write("v[2]'s final value is ",v[2]);
29  end.
The (line numbered) output of this program would be:
 1  a is 10
 2  i is 1
 3  w is 10
 4  i is now 2
 5  w is now 10
 6  a is now 30
 7  v[1] is now 10
 8  v[2] is now 20
 9  a's final value is 30
10  v[1] is 10
11  v[2] is 20
12  i is 1
13  w is 10
14  i is now 2
15  w is now 20
16  a is now 30
17  v[1] is now 10
18  v[2] is now 60
19  v[1]'s final value is 10
20  v[2]'s final value is 60
A few notes are definitely in order:
  • the part of the program involving the scalar variable a are the same as for the call by reference example in the previous section.

  • the part involving the array v is fundamentally different than the same part of the call by reference example in the previous section. Specifically, when byname is called with v[i] as the argument then changes to the global variable i while inside byname change which element of v is accessed/modified when w is accessed/modified.

    For example, the write call on program line 7 produces output line 13 saying that w is 10 because i is then 1 and w is a call by name parameter associated with v[i] and v[1] is 10. Output line 15 reports that w is now 20 because i is 2 when the write call on program line 9 is executed and hence v[i] or v[2] is 20.

    You may find it useful to now go back to the call by reference example and compare the output of the part of the program that used the v array with the equivalent part of the above call by name example.

Another issue surrounding the call by name mechanism is the question of lvalues vs rvalues. A true call by name mechanism allows the argument to be an rvalue. An attempt to assign a value to a call by name parameter which is currently associated with an rvalue would, presumably, result in a runtime error. Here's an example program to in which the argument is an rvalue which is not an lvalue:
 1  begin
 2     integer i;
 3     procedure byname2(integer name w)
 4        begin
 5        write("w is ",w);
 6        i = i + 2;
 7        write("w is now ",w);
 8        w = 127;
 9     end;
10   i = 5;
11   byname2(i * 3);
12   write("i's final value is ",i);
13   end.
The output of this program would be:
w is 15
w is now 21
*** runtime error on line 8: attempt to assign to an rvalue

There's an additional subtlety of call by name which needs to be discussed. When a call by name parameter is used within the procedure, the corresponding argument is evaluated in the context or scope of the calling routine.

For example, consider the following program:

 1  begin
 2     integer i;
 3     integer v[10]
 4     procedure byname3(integer name w)
 5        begin
 6        integer i;
 7        write("w is ",w);
 8        i = 2;
 9        write("w is now ",w);
10        end;
11     v[1] = 10;
12     v[2] = 20;
13     i = 1;
14     byname3(v[i]);
15  end.
One might expect that the assignment of 2 to i on line 8 would cause the output of this program to be
w is 10
w is now 20
This would be incorrect. The actual output of this program would be
w is 10
w is now 10
The argument is evaluated in the caller's context which results in the i declared on line 2 and initialized to 1 on line 13 being used to evaluate v[i] which yields 10 (since that i is 1). The i declared on line 6 and initialized to 2 on line 8 plays no role in the evaluation of v[i].

The modern Ruby language provides call by name parameter passing via mechanism which they call an iterator (see Jensen's device for more info).

In case you're wondering, the way to implement call by name is to use a thunk.

Spitbol, an historical programming language which is a variant of SNOBOL, used call by value for passing parameters although it also supported a name datatype which could be used to implement the equivalent of a call by value mechanism (the Spitbol name datatype could only be applied to the Spitbol equivalent of an lvalue).


A strange example

Let's take a look at a rather bizarre consequence of how certain older(?) FORTRAN compilers implemented call by reference and dealt with constants. Consider the following FORTRAN program:
      SUBROUTINE PRINTIT(J)
      WRITE(6,100) J
  100 FORMAT('J is ',I3) 
      RETURN
      END

      SUBROUTINE STRANGE(N)
      N = 10
      RETURN
      END

      CALL PRINTIT(3)
      CALL STRANGE(3)
      CALL PRINTIT(3)
      STOP
      END
In certain older(?) implementations of FORTRAN (e.g. Fortran-G on the IBM mainframe operating systems of the 1970s), the output of this program would be
J is   3
J is  13
The reason is two-fold:
  • the parameter passing mechanism used by older FORTRAN implementations was call by reference
  • all three uses of the constant 33 in the main program would be placed into a single memory location.
The result is that although the value 3 is passed to PRINTIT on the first call, STRANGE changes the value of 3 to be 13 which means that the second call to PRINTIT reports that J is 13!

When someone told me this back in my Fortran-G days, I wrote a test program similar to the one above. It behaved as I've described - i.e. the constant 3 was changed to 13.

Really!

Now for the really ugly bit - I heard a war story once that claimed that someone had had to take advantage of this feature to fix a bug in a program that they were maintaining. Supposedly, they didn't have access to the source code for one of the routines. This routine used a constant 3 in a context where the value 4 should have been used. Fortunately(?), the poor soul trying to fix this problem was able to determine that the constant 3 was passed to a routine for which the source code was available. Since the call to the second routine occurred before the incorrect use of the constant 3 and there were no other uses of the constant 3 within the original routine, the person modified the source for the second routine to change the value of the caller's constant 3 to be 4.

Frankly, this sounds rather far-fetched and it describes a set of pretty strange circumstances (i.e. the source code for the first routine isn't available but the person is able to figure out that it only uses the constant 3 in the two key places - in the call to the second routine and where it should have been 4). i.e. This is almost certainly an urban myth.