When a procedure is called, the actual parameters (the arguments at the call site) have to be connected to the formal parameters (the names inside the procedure). Exactly how that connection is made is a language-design decision with surprisingly deep consequences: it determines whether the callee can change the caller's variables, whether two names can secretly refer to the same storage, and even whether an argument's expression is evaluated once, many times, or never. This is the theory of parameter passing, and it is one of the sharpest places where two languages that look alike behave completely differently.
Everything here builds on the
The organising question is what gets put in the formal's slot and when the actual is evaluated. Value and value-result copy an evaluated value; reference passes an address; name and need defer the evaluation itself.
The difference is easiest to feel with an increment. Under call-by-value the callee scribbles on a
private copy; under call-by-reference it scribbles on the caller's own cell. The visual shows the crux:
under reference the formal x and the actual a are two names for
one storage cell — they are aliases.
This model implements a tiny memory as an array of cells. Passing "by value" copies a cell's contents
into the formal; passing "by reference" passes the cell's index, so writes go straight back to
the actual. Watch the same increment and swap produce completely different
after-effects:
Call-by-value is the reason a classic C swap(a, b) that takes plain ints does
nothing — you swap the copies. To make swap work you must pass addresses (pointers in C, references in
C++), i.e. move to call-by-reference semantics.
The cleanest way to compare mechanisms is: after the call returns, what happened to the actual, and how many times was the argument expression evaluated?
| Mechanism | What goes in the formal | Effect on the actual | Seen in |
|---|---|---|---|
| Call-by-value | a copy of the value | none — actual is unchanged | C, Java, Python (for the binding), most defaults |
| Call-by-reference | the actual's address (alias) | changes are visible immediately | C++ &, Pascal var, Fortran |
| Call-by-value-result | a copy in, then copied back out | updated once, on return | Ada in out |
| Call-by-name | an unevaluated thunk | re-evaluated on each use | Algol 60; Scala => by-name |
| Call-by-need | a thunk, memoised | evaluated at most once | Haskell (lazy evaluation) |
A subtle point: call-by-reference and call-by-value-result agree in the absence of aliasing but can disagree when aliasing is present — reference sees intermediate writes immediately, while value-result only commits at the end. That gap is exactly why language standards must pin the mechanism down precisely.
Call-by-name is the strangest and most powerful. The argument is wrapped in a thunk — a parameterless closure that re-computes the actual in the caller's environment — and this thunk is invoked afresh every time the formal name appears. It is as if the argument text were pasted in at each use.
This trick — a single argument expression that sweeps an array because a shared index variable changes under it — is the famous Jensen's device. It is elegant and almost unreadable, which is roughly why call-by-name did not survive as a default; but its idea lives on in lazy evaluation and in every "by-name" or deferred argument in modern languages.
Java is always call-by-value — but for object types the value that is copied is a reference (a pointer to the object). So the callee can mutate the pointed-to object's fields and the caller sees it, which feels like call-by-reference; yet if the callee reassigns the parameter itself to a new object, the caller's variable is untouched — the giveaway that it was by-value all along. The precise phrasing is "call-by-value, where the value passed for an object is a reference." Python behaves the same way (sometimes called "call-by-object-sharing"). Knowing the exact rule is the difference between a subtle bug and a correct program.
Two traps hide in these mechanisms. First, aliasing under call-by-reference: if you
call f(x, x) where f(a, b) takes both by reference, then a and
b are the same cell — writing a silently changes b, and
code that assumed they were independent breaks. The same happens if a global and a by-reference
parameter name the same variable. Second, re-evaluation under call-by-name: because the
thunk fires on every use, an argument with side effects (or a costly computation) runs again and again.
Pass a[i++] by name and the index advances on every mention; pass an expensive call and you
pay for it repeatedly. Call-by-need fixes the cost by memoising, but call-by-name does not — and both
are surprising if you expected a value to be computed exactly once, up front.