Flow control

Rec.: use a block for the body of for, do, while, if and else, even if it contains only one statement (or no statement at all).

Rule: never use goto.

Rec.: avoid continue when possible.

Rec.: try to design functions with only one exit point at the end, do not use return in the middle of a function.

Rule: in a switch statement, use break to terminate the code that handles a case label, do not let it fall through to the next case.

Rule: in a switch statement, always include a default handler.



The C syntax allows considerable freedom in using some constructs such as for or switch statements. Unfortunately it is very common to make an incorrect use of this much freedom, resulting (as usual) in code that is considerably more difficult to read and sometimes in a plain bug.

The most frequently misused construct is probably the for statement, used as a substitute for a while loop with shortcuts for the initialization and looping condition. Although that may save typing a few characters, and the compiler does not complain, this is not what for loops are supposed to do. A for loop should be used only when the loop variable goes from an initial value to a termination value, being increased or decreased by a fixed amount after each iteration. All other cases should be converted to while or do/while loops:

    for( f=fopen("foo.log","r"), line=0; f != NULL && (fgets(buffer,sizeof(buffer),f) != NULL; line++ ) {
        printf( "%s", buffer );
    }

The code snippet above may in fact suffer from a too cute problem. Here:

    f = fopen( "foo.log", "r" );
    if( f != NULL ) {
        count = 0;
        while( fgets( buffer, sizeof(buffer), f ) != NULL ) {
            printf( "%s", buffer );
            count++;
        }
    }

is definitely less cute but easier to live with.

Another common cause of trouble is when the body of a loop consist of one line only. In this case the brackets are not strictly required, so the following code is perfectly valid:

    for( int i=0; i<10; i++ )
        // Initialize the array elements to zero
        a[i] = 0;

In practice, however, this form leaves much room for error and is a good candidate for a bug. This doesn't have to happen soon, bugs are patient and may linger for months or even years before manifesting themselves. When the moment comes, another array is added so the initialization code is updated:

    for( int i=0; i<10; i++ )
        // Initialize data array b to zero
        b[i] = 0;
        // Initialize the array elements to zero
        a[i] = 0;

Of course now a[i] has been kicked out of the loop so this array doesn't get initialized (and may generate an out of bounds access for good), even though the indentation suggests otherwise. You may think it is easy to spot the error in the simple code fragment above, but it does occur and has been found in real code too, where things are usually a lot less neat. Also, this is not limited to the for example above, but is also true and possibly even more so for if's and while's. By using a block in place of a single statement you prevent problems of this type in the future:

    for( int i=0; i<10; i++ ) {
        // Initialize the array elements to zero
        a[i] = 0;
    }

Rec.: use a block for the body of for, do, while, if and else, even if it contains only one statement (or no statement at all).

Everyone has heard that goto is evil and has to be avoided at all costs. Well... that's true, but why? In a few words, the answer is that goto takes away from our ability to understand how a program works.

When we write a program, we face the rather difficult task of describing the behavior of a dynamic process, a program in execution, with a completely static notation: the source code. This is possible because we use the static representation to visualize in our mind the dynamic behavior of the program well before it is actually executed. It is extremely important that the static notation allows us to describe the desired behavior in the most direct and natural way, because each unnecessary step makes it more difficult for us to understand the dynamic process and anticipate the result.

Structured programming is so good because it provides a notation that is very linear and easy to understand. You can read a piece of code and at runtime the program will do exactly what you read, in the order you read it. And although loops and function calls do require some more mental resources to keep track of them, that is easily within the grasp of everyone.

This is not so when goto is used. The linearity is lost and each time you encounter a label that can be the target of a jump you are left to wonder: what is the immediate past of the program, which path has it followed to arrive here? And not too much later: what the darn thing is supposed to do?

Rule: never use goto.

The fact that goto is somewhat dangerous has been known for many years, yet it has found its way into many languages because it was not always easy to come up with a good substitute. Pascal for example was born as quite a "pure" structured language, but had to concede to goto in a few situations, such as exiting to top level from nested loops or recursive function calls. Many subsequent languages provide alternatives that are considerably safer to use, such as break, continue and return. However, this doesn't mean that these can be used at will and in any situation, because in many cases they can be almost as hard as goto to follow and understand.

In particular continue is not always easy to follow and should be used with care. Anyway, one should also avoid the opposite error of using all means just to avoid one continue or break. This is not necessary, and a well placed continue is still preferrable to a convoluted workaround that doesn't use it. The only rules that always hold is: think about it, and design your code even in these small particulars.

Rec.: avoid continue when possible.

Another dangerous practice is that of exiting from a function from many different places. Practice has shown that the resulting return-rich code is quite likely to hide some resource leaks or other bugs in it. Unfortunately, this code is also difficult to debug. Backtracking to the end of the function, for example, doesn't help much when that point could have been reached from many different return's.

The best way to avoid these errors is to write function that have only one exit point at the end. Sometimes this requires an extra effort in design, which is usually well spent by the way; some other times it may not be possible at all, but it is better to come to this conclusion after having pondered the problem at least for a short while. As an example, a function may want to validate the arguments passed to it and exit immediately if an error is found. Here using some return is still preferrable to using a series of nested if's, taking care to place the validation code right at the beginning of the function and to separate it (visually) from the rest.

Rec.: try to design functions with only one exit point at the end, do not use return in the middle of a function.

The typical use of the switch statement is as a substitute for a sequence of if/else statements, where it makes for more readable code. The C syntax allows code to fall-through from one case to another, a feature that has traditionally been a good source of bugs. It is better to use switch in its simpler and original form, using break to terminate the code that handles a case without trying to reuse it for the next case.

It is also good practice to always include a default statement, even if it appears unnecessary at a first glance. Such a default case will then be extremely helpful in tracking bugs caused by the switch expression assuming a value that was not foreseen in advance, something that may occur in practice for the most different reasons such as a bug buried somewhere in the rest of the code, a change in a function checked in by another programmer, and so on . The cost for adding this extra statement is usually negligible as it takes just a bit of space and there is no performance loss.

Rule: in a switch statement, use break to terminate the code that handles a case label, do not let it fall through to the next case.

Rule: in a switch statement, always include a default handler.

Top: Table of content  |  Previous: Expressions

Copyright (c) 2003 Alessandro Scotti. All rights reserved.