CSE 341: Parameter passing

All our languages so far this quarter have had the same semantics for function calls:

  1. Evaluate the function value and arguments.
  2. Bind the argument names to the argument values. (For OO languages, this includes the self receiver argument, and usually its instance variables.)
  3. Evaluate the function body in the environment produced in step 2.

This is called call by value parameter passing, because the parameter names are bound to the values of the arguments.

This is not the only way to design the semantics of function calls, although it is probably the "best". There are several other ways to design parameter passing; some are used by popular languages today, but others are mostly of historical interest:

As previously noted, most languages use call-by-value. However, at least two widely used languages use call-by-reference and call-by-name.

C++ reference parameters

In C++, you can declare that a parameter should use call by reference by using the @ symbol. Consider the following:

// Takes an int by value
void valueF(int x) { x = x + 1; }

// Takes an int by reference.
void refF(int@ x) { x = x + 1; }

int main() {
    int a = 0;

    valueF(a);
    cout << a << endl;  // Prints "0\n"

    refF(a);
    cout << a << endl;  // Prints "1\n"

    return 0;    
}

In valueF, x is an ordinary local variable. Mutating it doesn't affect the caller's variable. In refF, x is a reference to the caller's variable. Therefore, updating it updates the caller.

C preprocessor macros

The C language defines a preprocessor, which is a mini-language that runs over the C source code and transforms it in some fairly simple ways before the C compiler itself runs. The preprocessor is used for a variety of things, including:

Macros are special functions that are defined in terms of rewriting the source code of the arguments. Here's an example of a macro:

#define MAX(x,y) (x > y ? x : y)

This macro resembles the ML function

fun max(x, y) = if x > y then x else y

However, when C code invokes a preprocessor macro, then the preprocessor rewrites the source code, replacing the left side with the right side. Hence, in the following:

int a = 0;
int b = 5;
int c = 7
int d = MAX(a+b, b+c);  // XXX

line XXX will get rewritten to:

int d = (a+b > b+c ? a+b : b+c)

Notice that the argument expressions get duplicated --- and hence re-evaluated --- wherever the body refers to the argument name. Therefore, C preprocessor macros effectively pass parameters using call by name.

This leads to some odd results. Consider the following invocation of MAX:

MAX(a++, b++)

This will get rewritten to:

(a++ > b++ ? a++ : b++)

The result is that either a or b will be incremented twice --- probably not what the programmer wanted!

Call by name and lazy evaluation

All the languages we've studied so far (except for C syntax macros above) use strict evaluation, in which expressions are evaluated prior to being passed to functions.

There are languages (of which Haskell is the most popular) that use lazy evaluation: in lazily evaluated languages, expressions are not evaluated until they are needed for the result of the program.

In lazy languages, defining infinite data structures (e.g., the sequence of all natural numbers) and calling infinitely recursive functions (e.g., a function that generates all the Fibonacci numbers) do not necessarily lead to nonterminating programs. As long as a program only "needs" some finite subset of that infinite results, the program will terminate.

Lazily evaluated languages are rather awkward to use unless, like Haskell, they are purely functional --- i.e., there are no side effects (no updatable data structures, etc.). Side effects require that the programmer reason about the order that things happen, and in lazy languages it's hard to tell when any computation may occur ("when it's needed" can be arbitrarily far in the source code from where the expression is written).

It turns out that in lazily evaluated, purely functional languages, call-by-name makes perfect sense, because it allows you to "postpone" evaluation further than call-by-value. The problem we demonstrated above, with C preprocessor macros, does not arise, because purely functional languages cannot mutate data.

Lazy evaluation is powerful and cool, and has many proponents. Unfortunately, there's no time in this class to cover it adequately. In the spirit of lazy languages, however, I place here a pointer to Haskell, which you can evaluate at your leisure: http://www.haskell.org.

Parameter passing: Who cares?

Good question. Historically, programming languages have used a variety of passing modes, and programming languages courses used to spend entire lectures discussing parameter passing. Today, it is widely acknowledged that call by value is (for strict languages) "right", and that other modes are of mostly historical interest.

(For this reason, we won't require you to remember the details of call-by-result and call-by-value-result in this class.)

The primary reason you should care is that when you're programming with the odd language that doesn't use purely call by value --- e.g., when you're using C++ reference parameters, or C preprocessor macros --- then you must remember to be extra-careful.