Call-by-name, by-reference, by-sharing

Value vs reference

There are a lot of parameter-passing strategies. Let’s start from two:

  • call-by-value, the argument for a function parameter is a copy of the value of the argument
  • call-by-reference, the function is given the address of the argument

For example, C uses call-by-value strategy for all arguments except pointers, for which it uses call-by-reference

Variable assignment

Described strategies are always mentioned in the context of the variable binding for function calls, but they apply to all variable bindings e.g. assignments as well.

1int a = 1;
2int* b = malloc(sizeof(*b));
3int* c = malloc(sizeof(*c));

call-by-value

1*b = a;
2// it copies value, so change of variable `a` isn't reflected in `b`
3a = 2;
4printf("a = %d, b = %d\n", a, *b);

call-by-refernce

1b = &a;
2// change of variable `a` is reflected in `b`
3a = 3;
4printf("a = %d, b = %d\n", a, *b);

1// and vice versa - change of variable `b` is reflected in `a`
2*b = 4;
3printf("a = %d, b = %d\n", a, *b);

One more example of call-by-reference

1b = c;
2// change of variable `b` is reflected in `c`
3*b = 5;
4// but change of variable `a` isn't reflected in `b` anymore
5a = 6;
6printf("a = %d, b = %d, c = %d\n", a, *b, *c);

We can think about the next example as call-by-reference, which creates a reference to a new “anonymous” variable

1// allocates new memory cell for b
2b = malloc(sizeof(*b));
3*b = 7;
4printf("a = %d, b = %d, c = %d\n", a, *b, *c);

Function call

 1#include<stdio.h>
 2
 3void modify(int p, int* q) {
 4    p = 10; // passed by value
 5    *q = 20; // passed reference
 6}
 7
 8int main() {
 9    int a = 0;
10    int b = 0;
11    modify(a, &b);
12    printf("a = %d, b = %d, c = %d\n", a, b); // a = 0, b = 20
13    return 0;
14}

Call-by-sharing

Now the confusing part. Most popular languages, for example, Python, JavaScript, and Java use call-by-sharing. And people often confuse it with call-by-reference.

We call the argument passing technique call by sharing because the argument objects are shared between the caller and the called routine. … it is similar to argument passing in LISP. – CLU Reference manual

In call-by-sharing:

  • primitive values, like numbers, booleans and similar behave, like in call-by-value
  • non-primitive values, like objects, arrays and similar behave, like in call-by-reference
    • when you mutate reference you mutate all “aliases”
    • when you reassign reference you change “alias” - variable will contain a new reference and you can’t reassign values in other “aliases”
    • it is not possible to create a reference to a reference, like int**

In case of function calls it means that you can mutate variable in caller scope (because they are “shared”), but you can’t reassign them in caller scope:

call-by-referencecall-by-sharingcall-by-value
can reassign in caller scope+ (see note)--
can mutate in caller scope++-

Note:

  • to reassign, for example, integer or struct you need to use pointer int*, struct Test*
  • to reassign, for example, an array you need to use “double” pointer int**

Note 2: “primitive values” are immutable

Immutability and reassignment

If we would prohibit mutations and reassignment (like in many pure functional languages) 3 given strategies will look the same. Call-by-value is more expensive though because it needs to copy values.

Code examples

call-by-reference

 1#include<stdio.h>
 2#include <stdlib.h>
 3
 4void mutate(int* a) {
 5    a[0] = 2;
 6}
 7
 8// doesn't work
 9void reassign(int* a) {
10    int b[1] = {3};
11    a = b;
12}
13
14void reassign2(int** a) {
15    int b[1] = {4};
16    *a = b;
17}
18
19struct Test {
20   int counter;
21};
22
23void reassign3(struct Test* c) {
24    struct Test d = {counter: 5};
25    *c = d;
26}
27
28int main() {
29    // same as int a[1] = {1};
30    int* a = malloc(sizeof(a));
31    *a = 1;
32
33    mutate(a);
34    printf("a[0] = %d\n", a[0]);
35    reassign(a);
36    printf("a[0] = %d\n", a[0]);
37
38    int** b = malloc(sizeof(b));
39    b = &a;
40    reassign2(b);
41    printf("a[0] = %d\n", a[0]);
42
43    struct Test c = {counter: 0};
44    reassign3(&c);
45    printf("c.counter = %d\n", c.counter);
46    return 0;
47}

Call-by-sharing:

 1function mutate(a) {
 2  a[0] = 2;
 3}
 4
 5// doesn't work
 6function reassign(a) {
 7  a = [3];
 8}
 9
10// not possible
11// function reassign2(a) {}
12// function reassign3(c) {}
13
14let a = [1];
15
16mutate(a);
17console.log(a);
18reassign(a);
19console.log(a);

PS

If you want to read more about other parameter-passing strategies, I recommend this resource.

Except where otherwise noted, content on this site is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0