As others have said, the problem isn't with the goto
itself; the problem is with how people use goto
, and how it can make code harder to understand and maintain.
Assume the following snippet of code:
i = 4;
label: printf( "%d\n", i );
What value gets printed for i
? When does it get printed? Until you account for every instance of goto label
in your function, you can't know. The simple presence of that label destroys your ability to debug code by simple inspection. For small functions with one or two branches, not much of a problem. For not-small functions...
Way back in the early '90s we were given a pile of C code that drove a 3d graphical display and told to make it run faster. It was only about 5000 lines of code, but all of it was in main
, and the author used about 15 or so goto
s branching in both directions. This was bad code to begin with, but the presence of those goto
s made it so much worse. It took my co-worker about 2 weeks to puzzle out the flow of control. Even better, those goto
s resulted in code so tightly coupled with itself that we could not make any changes without breaking something.
We tried compiling with level 1 optimization, and the compiler ate up all available RAM, then all available swap, and then panicked the system (which probably had nothing to do with the goto
s themselves, but I like throwing that anecdote out there).
In the end, we gave the customer two options - let us rewrite the whole thing from scratch, or buy faster hardware.
They bought faster hardware.
Bode's rules for using goto
:
- Branch forward only;
- Do not bypass control structures (i.e., do not branch into the body of an
if
or for
or while
statement);
- Do not use
goto
in place of a control structure
There are cases where a goto
is the right answer, but they are rare (breaking out of a deeply nested loop is about the only place I'd use it).
EDIT
Expanding on that last statement, here's one of the few valid use cases for goto
. Assume we have the following function:
T ***myalloc( size_t N, size_t M, size_t P )
{
size_t i, j, k;
T ***arr = malloc( sizeof *arr * N );
for ( i = 0; i < N; i ++ )
{
arr[i] = malloc( sizeof *arr[i] * M );
for ( j = 0; j < M; j++ )
{
arr[i][j] = malloc( sizeof *arr[i][j] * P );
for ( k = 0; k < P; k++ )
arr[i][j][k] = initial_value();
}
}
return arr;
}
Now, we have a problem - what if one of the malloc
calls fails midway through? Unlikely an event as that may be, we don't want to return a partially allocated array, nor do we want to just bail out of the function with an error; we want to clean up after ourselves and deallocate any partially allocated memory. In a language that throws an exception on a bad alloc, that's fairly straightforward - you just write an exception handler to free up what's already been allocated.
In C, you don't have structured exception handling; you have to check the return value of each malloc
call and take the appropriate action.
T ***myalloc( size_t N, size_t M, size_t P )
{
size_t i, j, k;
T ***arr = malloc( sizeof *arr * N );
if ( arr )
{
for ( i = 0; i < N; i ++ )
{
if ( !(arr[i] = malloc( sizeof *arr[i] * M )) )
goto cleanup_1;
for ( j = 0; j < M; j++ )
{
if ( !(arr[i][j] = malloc( sizeof *arr[i][j] * P )) )
goto cleanup_2;
for ( k = 0; k < P; k++ )
arr[i][j][k] = initial_value();
}
}
}
goto done;
cleanup_2:
// We failed while allocating arr[i][j]; clean up the previously allocated arr[i][j]
while ( j-- )
free( arr[i][j] );
free( arr[i] );
// fall through
cleanup_1:
// We failed while allocating arr[i]; free up all previously allocated arr[i][j]
while ( i-- )
{
for ( j = 0; j < M; j++ )
free( arr[i][j] );
free( arr[i] );
}
free( arr );
arr = NULL;
done:
return arr;
}
Can we do this without using goto
? Of course we can - it just requires a little extra bookkeeping (and, in practice, that's the path I'd take). But, if you're looking for places where using a goto
isn't immediately a sign of bad practice or design, this is one of the few.