Functions, parameters, and procedural abstraction

Computer Science II · Lecture 1 ·

A function maps the argument 3 to the result 9.
The function square takes a value, performs a calculation, and returns a result.

A function is a named group of instructions for one task. Instead of repeating those instructions, another part of the program calls the function. The call can supply input values, receive a result, or ask the function to change an existing object. These are separate possibilities. Knowing which one a function promises makes its code easier to follow.

If declarations, expressions, or references are unfamiliar, first read CS1: functions and references. This lecture adds a closer look at what happens between the caller and the called function.

Reading a function's interface #

An interface tells you how to use a function before you read its implementation. In double square(double x), square is the name. The first double says that the function returns a floating-point number. The parentheses contain one parameter: another double, named x inside the function. A parameter is the local name used to receive an input. An argument is the actual expression supplied at a call.[1]

A declaration announces this interface. A definition also supplies the instructions inside the braces. A call asks the program to execute those instructions. Declaring a function does not run it. Defining it does not run it either: the call is what starts a particular execution.

double square(double x) {
    return x * x;
}
// In a calling function:
double side = 3.0;
double area = square(side); // 9.0

Following one call in small steps #

Read the example from the caller's point of view. The caller first creates side and stores 3.0 in it. The next statement cannot finish initializing area until the call to square finishes.

The call supplies side's value, 3.0. The function receives a local parameter x containing a copy of that value. Its return expression multiplies x by x, producing 9.0. The return ends this particular call and sends 9.0 back. That returned value takes the place of the call expression, so area is initialized to 9.0. Side still contains 3.0.

Returning and printing are different operations. This function computes a result but never sends text to the screen. A caller could print the result, store it, or use it as part of a larger expression. Conversely, a function can print a message without giving the caller a numerical result.

The braces also establish a local scope, meaning the part of the program where a name can be used. The caller uses side, while the function uses x. Those names do not need to match. Another function could have its own x or count; that would not make the variables shared.

Passing a copy or giving access to the original #

A value parameter receives a copy of its argument's value. Reassigning that parameter changes the local copy. A reference parameter is different: it is an additional name for the caller's object. A change through the reference changes that same object.

void double_in_place(int& value) {
    value *= 2;
}

Here the & belongs to the parameter declaration. It says that value refers to an existing integer. Suppose the caller has an integer containing seven and passes it to this function. There is no separate integer copy for value. Multiplying value by two updates the caller's integer to fourteen.

The call still uses the ordinary variable name. The caller does not add an ampersand simply because the function's parameter is a reference. The declaration determines how the argument is received.

The return type void means the function supplies no result value. It is useful because its documented effect is a change to the argument. Do not confuse that effect with returning fourteen: this example changes an object but returns no numerical expression to the caller.

Deciding what the function should promise #

A useful contract states valid inputs, the result, and any allowed changes. For square, the task is to calculate from its input without intentionally changing the caller's variable. For double_in_place, changing the caller is the task. These choices should be deliberate, rather than surprises hidden inside an otherwise harmless-looking call.

A mean function, for example, might require a nonempty collection. That requirement belongs in its contract, because there is no average to return from an empty set of inputs under the usual definition. The caller must meet the requirement or the function must explicitly handle that case.

Use parameters to express the data a task needs. Keep temporary work inside the function. This lets you test one calculation at a time and reuse it without copying its implementation into every caller. The aim of procedural abstraction is practical: once a function's behavior is understood, the caller can work with its promise rather than rereading every internal statement.

Practice and explanation #

Suppose side begins at four and is passed to square. What does area receive, and does side change? Area receives sixteen; side remains four because square uses a copied value parameter.

Now suppose an integer begins at four and is passed to double_in_place. It becomes eight because the reference parameter names the caller's integer. There is no returned integer to assign. When tracing a new function, answer these two questions separately: what value comes back, and which existing objects may change?

References

  1. ↑ C++ working draft: functions .