Issue
Best programming practices recommend writing self-contained functions, meaning that all the variables read or written are either parameters of the functions or declared as local variables in the function body. This facilitates reasoning about the flow of data in and out of the function, which helps from the point of view of maintainability, testing and compiler optimization.
Relevance
Using global variables makes it hard to reason about where and how those variables are used. By including all data inputted and outputted by a function in its signature, the interactions of the function with external data can be easily determined by inspecting the function call sites.
As a result it is easier to identify the function’s side effects. A function modifying a global variable can easily go unnoticed since the function body must be inspected to detect it. This is critical in case the function is to be executed in parallel since it may lead to a race condition, making the result of the code unpredictable.
Actions
Explicitly pass all required data as parameters including output variables. In case there is a single output variable, use the function return value instead of a parameter.
Notes
In large projects, it can be difficult and time consuming to apply this recommendation because of the large number of changes. An alternative would be to outline the critical loop into a separate function.
Code example
In the following example, the flow of the data across function calls is not explicit. The global variables A and B are read or written in functions init
and add
. As the functions have no parameters, it is necessary to keep track of the read and write operations to A and B across multiple functions (and in real codes even across multiple files).
#define N 100
int A[N];
int B[N];
void init() {
for (int i = 0; i < N; i++) {
A[i] = i;
}
}
void add() {
for (int i = 0; i < N; i++) {
B[i] = A[i] + 1;
}
}
void foo() {
init();
add();
}
When you pass the array explicitly as a parameter to the function, with a simple glance at the code you notice which are the arrays involved during the execution of function foo
. In the refactored code the inputs and outputs of the function calls init(A)
and add(A,B)
are explicit, so it is easier to determine the flow of data at runtime, and thus to determine the function’s side effects.
#define N 100
int A[N];
int B[N];
void init(int *A) {
for (int i = 0; i < N; i++) {
A[i] = i;
}
}
void add(int *A, int*B) {
for (int i = 0; i < N; i++) {
B[i] = A[i] + 1;
}
}
void foo() {
init(A);
add(A, B);
}
Related resources
- PWR001 examples at GitHub
- G.J. Holzmann (2006-06-19). “The Power of 10: Rules for Developing Safety-Critical Code”. IEEE Computer. 39 (6): 95–99. doi:10.1109/MC.2006.212. See Rule 6: “Declare all data objects at the smallest possible level of scope”. [last checked May 2019]
References

Building performance into the code from day one with Codee