Subprograms I: parameter passing methods
Formal/actual parameters, the in/out/inout models and five implementations: pass-by-value, result, value-result, reference, name. Tracing what the same program prints under each method; also Java's rule, closures and default parameters.
This week’s topic, parameter passing methods, is one of the most “mechanical” topics on the exam and one of the biggest point winners. The question template is always the same: a small procedure, a call, “what happens to the variables under each method?” Recipe: for each method write what the formal parameter receives (a value, an address, an expression) and when it writes back, then trace line by line.
Golden rules:
- Value: a copy. Reference: an address. One does not change the outside, the other changes it immediately.
- Value-result changes the outside only at return, and the result may depend on the copy order.
- Name: re-evaluate the expression on every use. If the index variable changes, you go to a different element.
- If the same variable is passed to two parameters, or as parameter + global, there is an alias. Most questions come from here.
- In Java everything is pass-by-value, including references.
Basic concepts
A subprogram definition consists of a header (name, parameters) and a body. The parameters in the definition are formal parameters, the expressions in the call are actual parameters.
- Positional: actuals are matched in order. Short, but causes mistakes in long lists.
- Keyword:
area(width = 3, height = 5)(Python, Ada). The order is free, but the formal names must be known. - Default: the value used if no actual is given.
function f(a, b = a * 2) { return a + b; }
console.log(f(3), f(3, 1));
9 4
Subprograms are divided into functions, which return a value, and procedures, which do not. Local variables are usually stack-dynamic (recursion is possible); in C they can be made static with static (they remember history).
Semantic models: in, out, inout
| Model | Data flow | Implementation methods |
|---|---|---|
| In mode | From caller to callee | Pass-by-value |
| Out mode | From callee to caller | Pass-by-result |
| Inout mode | Two-way | Pass-by-value-result, pass-by-reference, pass-by-name |
Implementation is done in two ways: physically copying the value (value, result, value-result) or transferring an access path (address) (reference).
Five methods
Pass-by-value
The actual’s value initializes the formal, and the formal behaves like a local variable. No effect on the outside. Cost: large objects are copied.
Pass-by-result
The input value is not transferred. The formal is initialized in the callee, and its value is copied to the actual on return. Problems: in calls like g(a, a), which copy will win (actual parameter collision), and whether the address of the actual is computed at the call or at the return.
void g(out int x, out int y) { x = 5; y = 7; }
g(a, a); copy left to right: a = 5, then a = 7 → 7
Pass-by-value-result
Value + result: copy in at entry, copy out at return. Similar to reference but alias effects are not visible until the return.
Pass-by-reference
The address of the actual is transferred, and the formal becomes its alias. No copy, fast. But aliases lower readability and reliability; every access needs an indirection.
Pass-by-name
Every use of the formal behaves as if it were replaced by the text of the actual, and is evaluated at that moment in the caller’s environment. It exists in ALGOL 60 and is rarely used today. Its implementation: a thunk for each parameter (a small function that computes the address or value when called). When a simple variable is passed it behaves like reference; the difference appears when an array element or an expression is passed.
Worked example 1: the same variable twice
void f(int x, int y) { x = x + 1; y = y * x; }
a = 2; f(a, a);
| Method | Trace | Final a |
|---|---|---|
| Value | x = 2, y = 2 copies; a is untouched | 2 |
| Reference | x and y = the cell of a. a = 3, a = 3 · 3 | 9 |
| Value-result (left to right) | x = 3, y = 2 · 3 = 6; return: a = 3, then a = 6 | 6 |
| Value-result (right to left) | the same, return: a = 6, then a = 3 | 3 |
| Name | x and y are a on every use: same as reference | 9 |
Worked example 2: alias with a global
int g = 3;
void fun(int x) { x = x * 2; g = g + 1; }
fun(g);
value: x = 3 → 6 (local) g = 3 + 1 → 4
reference: x ≡ g: g = 6 g = 6 + 1 → 7
value-result: x = 3 → 6 (local) g = 4; on return g = x → 6
name: x ≡ g (simple variable) → 7
Worked example 3: array element and a changing index
int i = 0; int A[3] = {1, 2, 3};
void h(int x) { i = i + 1; x = x + 10; }
h(A[i]);
value: the array does not change {1, 2, 3}
reference: address at the call: x ≡ A[0] {11, 2, 3}
the change of i is irrelevant
name: x is A[i] on every use; i is now 1 {1, 12, 3}
x = x + 10 → A[1] = A[1] + 10
With value-result you also get {11, 2, 3}, because the return address is computed at call time. In some definitions the address is computed at return; then it would be {1, 11, 3}. If the question does not specify it on the exam, assume call time and write that down.
Worked example 4: the classic swap(i, A[i])
void swap(int x, int y) { int t = x; x = y; y = t; }
i = 1; A = {5, 0, 7};
swap(i, A[i]);
reference (addresses at the call: x ≡ i, y ≡ A[1]):
t = i = 1; i = A[1] = 0; A[1] = t = 1 → i = 0, A = {5, 1, 7} correct swap
name (re-evaluate on every use):
t = i → 1
i = A[i] → i = A[1] = 0
A[i] = t → A[0] = 1 ← i is now 0
→ i = 0, A = {1, 0, 7} swap BROKEN
A general swap that works correctly cannot be written with pass-by-name. One of the reasons this method was abandoned.
PITFALL · thinking name and reference are the same. When a simple variable (
a) is passed, the two give the same result. When an array element is passed and the index changes in the body, they differ: reference writes to the old element, name writes to the new element.
The situation in languages
| Language | Rule |
|---|---|
| C | Pass-by-value. For a reference effect a pointer is passed (swap(&x, &y)) |
| C++ | Value + reference types (int&); const int& passes large objects without copying but without modification |
| Java | Everything is pass-by-value. Because object variables hold references, a copy of the reference is passed |
| C# | Default value; ref for reference, out similar to result |
| Ada | in, out, in out modes are written explicitly |
| Python, Ruby | Pass-by-assignment: the reference to the object is copied (similar to Java) |
static void swap(int a, int b) { int t = a; a = b; b = t; }
static void change(int[] arr) { arr[0] = 99; arr = new int[]{7, 7}; arr[1] = 55; }
static void grow(StringBuilder sb) { sb.append("!"); sb = new StringBuilder("new"); sb.append("?"); }
int x = 1, y = 2; swap(x, y);
System.out.println(x + " " + y);
int[] a = {1, 2}; change(a);
System.out.println(a[0] + " " + a[1]);
StringBuilder s = new StringBuilder("hi"); grow(s);
System.out.println(s);
1 2
99 2
hi!
Rule: a method can change the inside of an object (because it points to the same object), but cannot point the caller’s variable at a different object.
Closures and other topics
A closure is the combination of a subprogram and the referencing environment where it was defined. If a function can be returned as a value and called elsewhere, the lifetime of the variables it captured must be extended (unlimited extent).
function makeCounter() { let c = 0; return () => ++c; }
const c1 = makeCounter(), c2 = makeCounter();
console.log(c1(), c1(), c2(), c1());
const fs = []; for (var i = 0; i < 3; i++) fs.push(() => i);
console.log(fs.map((g) => g()).join(","));
const gs = []; for (let j = 0; j < 3; j++) gs.push(() => j);
console.log(gs.map((g) => g()).join(","));
1 2 1 3
3,3,3
0,1,2
- Overloaded subprogram: the same name, different parameter profiles (Java, C++). Which one is called is determined at compile time.
- Generic subprogram: a single definition that works with different types (Java generics, C++ templates). Parametric polymorphism.
- Variable number of parameters: Java
int... xs, Cprintf.
Common mistakes
| Wrong | Correct |
|---|---|
| Value-result = reference | The alias effect is not visible until return; f(a, a) gives a different result |
| Copy order is irrelevant in value-result | It matters in f(a, a): left to right 6, right to left 3 |
| Name = reference | They differ with a changing index |
| In reference the address is computed on every access | Once, at call time |
| Java objects are pass-by-reference | A copy of the reference; arr = new does not change the outside |
| In result mode the input value is used | The formal is not initialized |
| A line that changes a global is ineffective under value | The global changes directly; only the formal is a copy |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
Given the procedure below and a global a = 2, what is the value of a after the call f(a, a) under (a) pass-by-value, (b) pass-by-reference?
void f(int x, int y) {
x = x + 1;
y = y * x;
}Show answer
Value: x and y are copies of a, a is untouched: 2. Reference: x and y are both bound to the address of a (the same cell, aliases). x = x + 1 → a = 3. y = y * x → a = 3 * 3 = 9.
Same procedure and call f(a, a) with a = 2, but pass-by-value-result, copying back from left to right. What is a? What if copying is right to left?
Show answer
At entry x = 2, y = 2 (copies). x = 3. y = 2 * 3 = 6. On return copy back: first x → a (a = 3), then y → a (a = 6). If you copy from right to left, first y (6), then x (3): a = 3. When the same variable is passed twice, the result depends on the copy order; the language must define this.
Using pass-by-result with g(a, a), where void g(out int x, out int y) { x = 5; y = 7; }, and copying back left to right, what is a?
Show answer
In pass-by-result the input value is not transferred, x and y are initialized in the callee. On return x → a (5), then y → a (7). The last copy wins. This situation is called an actual parameter collision: the result depends on the copy order, and if the language does not define this order, the program is not portable.
With i = 1 and A = {5, 0, 7} (0-based), what are i and A after swap(i, A[i]) using pass-by-name?
void swap(int x, int y) {
int t = x;
x = y;
y = t;
}Show answer
In pass-by-name every use of x and y is textually replaced with i and A[i] and evaluated at that moment.
t = i t = 1
i = A[i] i = A[1] = 0
A[i] = t A[0] = 1 (i is now 0!)The swap fails: the desired result was i = 0, A[1] = 1. The same call with reference does the swap correctly: i = 0, A = {5, 1, 7}.Global i = 0, A = {1, 2, 3}. What is A after h(A[i]) under (a) reference, (b) name?
void h(int x) {
i = i + 1;
x = x + 10;
}Show answer
Reference: the address is computed at call time, x = the address of A[0]. The change of i does not affect x: A[0] = 1 + 10 = 11. Name: x is re-evaluated as A[i] on every use; i is now 1: A[1] = A[1] + 10 = 12. With value the array does not change.
Global g = 3. What is g after fun(g) under value, reference and value-result?
void fun(int x) {
x = x * 2;
g = g + 1;
}Show answer
Value: x is a copy, only g = g + 1 is effective: 4. Reference: x and g are the same cell (alias): x = 6 → g = 6, then g = 7. Value-result: x = 3 copy, x = 6; g = 4; on return x is copied to g and overwrites g = 4: 6.
What is printed in Java?
static void swap(int a, int b) { int t = a; a = b; b = t; }
// in main:
int x = 1, y = 2;
swap(x, y);
System.out.println(x + " " + y);Show answer
In Java all parameters are pass-by-value. a and b are copies of x and y; the swap happens between the copies. In Java you cannot write a method that swaps two ints.
What is printed in Java?
static void change(int[] arr) {
arr[0] = 99;
arr = new int[]{7, 7};
arr[1] = 55;
}
// in main:
int[] a = {1, 2};
change(a);
System.out.println(a[0] + " " + a[1]);Show answer
The reference itself is passed by value. arr[0] = 99 changes the same object, visible from a. arr = new ... only points the local copy at another object; a still points to the old array. arr[1] = 55 is written to the new array. The result is 99 2.
What does this JavaScript code print?
function makeCounter() { let c = 0; return () => ++c; }
const c1 = makeCounter(), c2 = makeCounter();
console.log(c1(), c1(), c2(), c1());Show answer
Closure: the returned function carries along the variable c of the environment where it was created. The lifetime of c does not end when makeCounter returns (unlimited extent). Each call of makeCounter creates a separate c: c1's counter is 1, 2, 3; c2's is 1.
What does this JavaScript code print?
function f(a, b = a * 2) { return a + b; }
console.log(f(3), f(3, 1));Show answer
Default parameter: if no actual is given, b = a * 2 = 6, 3 + 6 = 9. If given, it is used: 3 + 1 = 4. Default parameters exist in Python, C++, Ada, JavaScript; not in Java (imitated with overloading).
Which parameter passing method is described: (a) the actual's value initializes the formal, no value is sent back; (b) the formal is an alias of the actual; (c) the actual is textually substituted and re-evaluated at every reference; (d) value copied in at the call and copied out at the return.
Show answer
Value = in mode, a physical copy. Reference = inout, an access path (address) is transferred. Name = inout, late binding, implemented with a thunk. Value-result = inout, two-way copy; sometimes called pass-by-copy. Result = out mode.
What is printed in JavaScript?
const fs = [];
for (var i = 0; i < 3; i++) fs.push(() => i);
console.log(fs.map(g => g()).join(","));Show answer
All three closures capture the same var i (var has function scope, a single variable). When they are called the loop has ended, i = 3. If it were written with let j, each iteration would be a separate variable and 0,1,2 would be printed.
Verification: The five parameter passing methods were simulated in node with a real memory model (cells, addresses, thunks re-evaluated on every access for by-name) and the program in each question was run in this simulation (verify/SEN2001/08.mjs); the copy-back order and whether the address is computed at the call or at the return were also tested. Java's pass-by-value behavior was run with the JDK (java verify/SEN2001/W08.java), closures and default parameters with node (verify/SEN2001/08b.mjs).