6.1. Functions

Time: 00:06:09 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

Along with variables, functions are among the oldest and the most important programming constructs in imperative programming languages. Like variables, programmers name functions, and the compiler maps the function names to the function's memory address (i.e., the memory location of the function's machine code instructions). Function calls can pass zero or more arguments to functions, which operate on the data and return zero or one value to the call. While the returned value can be an aggregate type (an array or object), we still consider it a single value.

A figure illustrating a function viewed both as a single statement and as a path moving through a program as one function calls another and as functions return to the statement following the call. An abstract representation of a function as a rectangle. Arrows illustrate that a function call jumps from the call to the function's instructions while passing the arguments in the call to the function's parameters. Arrows also illustrate that when the function ends, control returns to the statement following the function call.
(a)(b)
Visualizing function calls. We can view a function call as another sequential statement performing a single, well-defined operation on a set of input arguments. In this view, we treat it as a black box, "hiding" its internal operations. Alternatively, we can view a function call as interrupting the "normal" sequential instruction flow, treating it as a white box by exploring its internal operations.
  1. The dashed gold arrow illustrates our view of the function call as a simple, sequential statement. Our previous uses of the sqrt and pow functions demonstrates this view. The solid black arrows demonstrate how the function calls jump to different memory locations, interrupting the program's sequential execution flow. The program executes the function's instructions and returns to the call when the function ends. The solid arrows also demonstrate that one function can call another and that functions return in the reverse order of their calls. Beyond the memory available to the program, C++ doesn't impose a syntactic limit on the length of the function call chain.
  2. A program may call a function many times, passing different data through the call's arguments (e.g., a, b, c, and d) each time. Along with their parameters (e.g., x and y), functions perform the same operations each time the program calls them but with potentially different data.

Although functions impact programming in many ways, their most important contribution to software development is their influence on how developers conceptualize or think about problems and how they implement the software that solves them. Trying to understand a large and complex problem in its entirety is both overwhelming and counterproductive. Software developers typically decompose or break down the problem into successively smaller units until they reach a conceptual unit that is small enough and simple enough to understand. Once they understand the smaller units, they build increasingly complex units composed of the smaller, more simple units.

Similarly, software design based on the procedural programming paradigm proceeds by representing or mapping parts of the problem to corresponding functions. Software designers decompose or break functions into increasingly smaller and more simple functions. Unfortunately, there is no "magic" recipe for doing a functional decomposition, and there is no perfect size for the functions at which we stop the decomposition. We continue breaking the functions down until we reach a point at which we can understand and conveniently implement the individual functions.

Mapping parts of a problem to different functions implies that we can use functions to think about and represent abstract problems. As Jonassen suggests, we can then manipulate those functions as part of our problem-solving process. Finally, we implement or program those functions to realize a working solution to the original problem.

Functional decomposition represented as an inverted tree. The most complex function is at the top (the root) and a set of smaller, more simple functions at the bottom (the leaves).
An abstract representation of a functional decomposition. Each box represents a function that calls the successively smaller, more simple functions that lie below it in the diagram. The leaf functions are typically small and simple and perform only single, well-defined tasks. Programs can also call functions in ways that result in cycles in the function-call diagram.

Software developers typically take one of two paths to implement the functions.

Bottom-Up Implementation

Bottom-up implementation begins with the leaf functions at the bottom of the tree. Once programmers implement the leaves of a sub-tree, they can implement the sub-tree itself. If the functions only rely on data passed in through the argument list, it is possible to validate these functions independently of the rest of the program. Programmers use a driver or set of driver programs to validate each function. A driver is simply a program that calls a function, sends appropriate test data as arguments, and in some way verifies that the returned values or effects of the function are correct. The bottom-up strategy is demonstrated by both the Time and the American example programs presented in the last chapter.

For contemporary software development, the bottom-up approach is appropriate for small programs or for implementing the core functions of larger, more complex programs. However, the bottom-up approach is incompatible with incremental delivery for large, dynamic programs. Software developers practice incremental delivery when they initially deliver a system with essential core features and incrementally add and deliver enhancements over time.

void function(int x, int y, int z)
{
	.
	.
	.
}

int max(int x, int y)
{
	return (x > y) ? x : y;
}
int main()
{
	function(10, 20, 30);
	int a = max(100, 200);
	cout << a << endl;

	return 0;
}



(a)(b)
Bottom-up implementation. Programmers implement functions from the bottom of the tree to the top, beginning with the functions that do not call or rely on other functions. Programmers write a driver function that calls and tests these functions and then discard it once the testing and validation steps are complete. Driver functions are temporary and do not contribute to solving the program problem, so it's okay to hard code test values in them.
  1. An example of simple leaf functions.
  2. A driver program.

Top-Down Implementation

Top-down implementation begins with the high-level functions. Function stubs replace the smaller, lower sub-functions. A stub is nothing more than an empty function where the body contains just enough code to allow the function to compile without error. For example:

void print(int x, double y)
{
}

double pow(double x, double y)
{
	return 1.0;
}
Stub functions in a top down implementation. Programmers implement the functions highest in the decomposition or tree first. Whenever one of these functions needs to call a function lower in the decomposition, the programmer creates a stub function. Stub functions contain just enough code to allow a program to compile and run - they are, in effect, temporary placeholders. Functions that return a value (i.e., that do not have void as the return type) must include a "dummy" return statement (e.g., return 1.0;) in the body for the program to compile. The bodies of stub functions are replaced by fully operational statements (and any dummy returns are discarded) as development continues downwards from the top of the decomposition tree.

Top-down implementation allows software developers to practice incremental delivery. Large software systems typically provide many related features. Programmers can implement a minimal core set of features that form a deliverable product capable of doing practical work. Programmers regularly enhance the product with additional features and incrementally deliver enhancements to customers and clients. Incremental delivery allows developers to gain market share and gather feedback about what is important to customers.