Passing arguments to a function

In this section we discuss how the value of the actual arguments in a function call are passed to the function being called. Consider again the function sum.

1
2
3
4
5
6
7
// Return the sum of integers from 1 to n
int sum(int n) 
{
    int result = (n * (n + 1)) / 2;

    return result;
}

First, it is important to keep in mind that functions only have access to the variables defined in the function’s body (e.g. result) and the functions formal arguments (e.g. n). The variables defined in the function’s body are called local variables (e.g. result is a local variable of function sum).

A function only has access to the variables defined in the function’s body and its formal arguments.

Consider the following main which calls sum(a) (line $7$).

1
2
3
4
5
6
7
8
9
int main() {
    std::cout << "Enter a:\n";
    int a;
    std::cin >> a;
	
    if (a > 0) {
        std::cout << sum(a); // call function sum for argument a
    }
}

Function sum does not have access to variable a because this variable is defined in the main. How does then function sum get access to the value of variable a? The answer is that call-by-value is used when function sum is called.

Call-by-value

Call-by-value is a technique used when a function is called that copies the value of the actual arguments into the variables representing the formal arguments of the function. In other words, the formal arguments of the function receive a copy of the corresponding actual arguments used in the function call. The correspondence between formal and actual arguents is done by position in the argument list, i.e. the first actual argument is copied into the first formal argument, the second actual argument is copied into the second formal argument, and so on.

Consider the example above and the function call in line $7$ of the main. Then, the formal argument n of function sum receives a copy of the int stored in the actual parameter a. Note that the function sum does not have access to variable a defined in the main function. Function sum has only access to a copy of a which is stored in the variable n.

Sum function.

Thus, using call-by-value implies that the function can modify the value stored in the formal argument, n, without modifying the actual argument, a. For instance, function sum could be written as follows, without running the risk of modifying the actual argument a.

1
2
3
4
5
6
7
// Return the sum of integers from 1 to n
int sum(int n) 
{
    n *= (n + 1);

    return n/2;
}

A disadvantage of call-by-value method is that it requires copying which consumes both extra memory space and time. Thus, call-by-value can be quite inefficient when applied to arguments that occupy many bytes. Another limitation of call-by-value is illustrated by the following example.

Assume one wants to write a function that swaps the values of two variables. This function is actually a very central function in many libraries, like the C++-standard library, because many important algorithms rely on swapping values of two variables (such as sorting functions). Let’s then make our first attempt to write such function. Bare in mind that the function below would not accomplish this task.

void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}

Consider the following code excerpt which calls function swap given above.

int main() {
    int a = 6;
    int b = 8;
	
    swap(a,b);  // variables a and b are not modified: a still stores 6 and b stores 8
	
    std::cout << "a = " << a
              << "  b = " << b << "\n";
}

Since call-by-value is used, the value of the actual arguments a and b are copied into the formal arguments of the function, x and y, repectively. The function swap does not have access to the variables a and b defined in the main. Instead, the function swaps the values in the variables x and y (i.e. the copies of a and b are swapped). The function swap provided above is just useless, though it compiles (and executes).

So, how can one write in C++ a function that swaps the values of two variables? The answer is to use call-by-reference, instead of call-by-value, to pass the arguments a and b to the function.

Call-by-reference

Call-by-reference is a technique used when a function is called where the formal arguments of the function refer to the actual arguments. In other words, the formal parameters of the function give access the the variables representing the actual parameters used in the function call. Consequently, the function called can modify the actual parameters which are variables outside the function.

Let’s re-write the function swap but now we use call-by-reference, instead of call-by-value.

1
2
3
4
5
void swap(int& x, int& y) {
    int temp = x;
    x = y;
    y = temp;
}

Observe the code carefully. Nothing changed in the function’s body, i.e. it’s still the same logic. The only modification is in the list of formal arguments of the function:

void swap(int& x, int& y);

The ampersand (&) is used before the formal arguments x and y. The use of the ampersand (&) before a formal argument indicates to the compiler that call-by-reference should be used for the argument when the function is called (instead, of using call-by-value).

Consider again the function call swap(a, b).

int main() {
    int a = 6;
    int b = 8;
	
    swap(a,b);  // variables a and b are modified
    // now variable a stores 8 and variable b stores 6
	
    std::cout << "a = " << a
              << "  b = " << b << "\n";
}

The formal arguments, x and y, refer to the actual arguments a and b, respectively. The execution of the statement x = y; (line $3$ of function swap) takes into account that x and y are kind of special variables that refer to other variables outside the function swap and the value of the variable referred by y (i.e. y refers to variable b) is stored in the variable referred by x (i.e. x refers to variable a). Thus, the statement x = y; (line $3$ of function swap) effectively modifies the value of the actual parameter a. A similar idea applies to statement y = temp; (line $4$ of function swap) stating that the value stored in variable temp is copied into the variable referred by y (thus, the value stored in b is modified). The figure below illustrates this idea.

swap function.

At first sight, the concept of a variable referring to another variable may seem quite vague (almost illusive). You may wonder but how does a variable refer to another variable? How is this implemented by the computer? Well, that’s business for the compilers and compilers may implement this concept in different ways. For instance, the formal arguments of function swap may receive the memory address of the actual arguments, i.e. x stores the memory address of variable a and y stores the memory address of variable b. In this way, function swap can access the actual arguments, a and b, which are variables defined outside the function. Indeed, a powerful method to pass information to functions!! But this power has also its dangers, if misused.

Consider again function sum but now using call-by-reference.

1
2
3
4
5
6
7
// Return the sum of integers from 1 to n
int sum(int& n) // Ohoops, call-by-reference is a bad idea here!!
{
    n *= (n + 1);

    return n/2;
}

Consider also the following main which includes a call to function sum. This piece of code will display “sum(272) = 136” which is not the correct output we would expect (the correct output is “sum(16) = 136”).

int main() {
    int a = 16;
    int result = sum(a); // Ohoops, variable a has unexpectedly been modified: a stores 272

    std::cout << "sum(" << a << ")=" << result;
}

The bug lies in the fact that the formal argument n of function sum gives access to variable a defined in the main. Thus, the statement n *= (n + 1); in the function (line $4$) modifies the value of the variable referred by n, i.e. variable a is modified.

Good programming practices

The table below compares and contrasts both methods used in C++ to pass arguments to functions: call-by-value and call-by-reference. Similar methods also appear in other languages such as C#, Java, PHP, Javascript, and Python.

  Call-by-value Call-by-reference
Meaning A copy of the actual arguments is passed to the function. The formal arguments refer to the actual arguments. Usually, the memory address of the actual arguments is passed to the function.
Advantages Actual arguments cannot be changed accidentally by the function. Efficiency. It does not require to make copies of variables.
Disadvantages Copying variables with many bytes consumes extra time and space. Accidental changes in a formal argument also affect the value of a variable outside the function.

The C++ core guidelines describes good programming practices about the use of both methods and the C++ community expects the programmers to follow these good practices. The table below gives a simple (yet incomplete) summary of some of those good practices. You are required to apply these good programming practices, too.

Call-by-value Call-by-reference
void f(T x) {
   //Do something with x
}
void update(T& x) {
   //Do something that modifies x
}
Use when type T is cheap to copy, i.e. T occupies few bytes (like int, double, bool), and one wants to ensure that the function does not modify the actual parameters. Use if the function should update the value stored in the variable referred by x.

In C++, if an argument x is passed by reference to a function f, then programmers assume that calling f will modify x.

To summarize: by default, C++ uses call-by-value to pass arguments (parameters) to a function. The programmer must explicitly indicate that call-by-reference should be used for an argument by writting the ampersand symbol (&) before the formal argument of the function. Additionally, the programmer must always have a well-motivated reason to use call-by-reference. Otherwise, call-by-reference should not be used.

One may wonder how to handle the case when a variable x occupying many bytes is passed to a function (like a vector storing a long list of items), though the function is not meant to modify x. This point is discussed in the next section.