Common Uses For Goto
Last time we looked at the
goto
statement. Today we will look at the two common uses
for goto
: flow control in nested loops and organizing
error handing code.
Flow control
As we saw in previous weeks, the
break
and
continue
statements are used to modify the flow of execution in a loop. Both
break
and continue
affect only the innermost
enclosing loop. Sometimes you need to break out of nested loops. You
could implement this with boolean flag values, but that approach can
make your loop logic more convoluted and error prone. Instead, a
goto
statement can be used like break
to jump
out of nested loops:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (...) {
goto doneWithLoops;
}
}
}
doneWithLoops:
// do more stuff
Similarly, you can use goto
to emulate
continue
within nested loops, though keep in mind that
goto
doesn't automatically advance to the next item in a
for
or for...in
loop the way
continue
does:
for (int i = 0; i < 10; i++) {
start_j:
for (int j = 0; j < 10; j++) {
if (...) {
i++; // manually advance loop counter to emulate continue
goto start_j;
}
}
}
Along with its use in loops, the break
statement is also
used in the switch
statement to mark the end of a
case
block. It's not uncommon to use a switch
statement inside a loop when implementing simple state machines, event
dispatchers and parsers:
// simple event dispatcher
MyEvent *event = nil;
while (event = getNextEvent()) {
switch (event.type) {
case KEY_EVENT:
// handle key event
break;
case MOUSE_EVENT:
// handle mouse event
break;
}
}
shutdown();
Sometimes you want to exit the event loop from within one of the
case
blocks, like this:
// simple event dispatcher
MyEvent *event = nil;
while (event = getNextEvent()) {
switch (event.type) {
case KEY_EVENT:
// handle key event
if (event.keycode == KEY_ESC) {
// want to break out of the while loop
// but a break here applies to the case block
} else {
// ...
}
break;
case MOUSE_EVENT:
// handle mouse event
break;
}
}
shutdown();
You can always use a boolean flag variable and make your loop test more
complex, but using goto
here can make your code simpler
and easier to follow:
// simple event dispatcher
MyEvent *event = nil;
while (event = getNextEvent()) {
switch (event.type) {
case KEY_EVENT:
// handle key event
if (event.keycode == KEY_ESC) {
goto event_loop_end;
} else {
// ...
}
break;
case MOUSE_EVENT:
// handle mouse event
break;
}
}
event_loop_end:
shutdown();
Error handling
Standard C doesn't have a concept of throwing and catching exceptions;
it's normal for functions in C libraries to return a result code to
indicate an error (or to have an out parameter that holds a result code
or error object). Writing robust programs using a C API requires
checking result codes at each step and taking the appropriate action.
For example, a function to copy a block of data from one file to
another might look like this:
void copy_block(char const *in_filename, char const *out_filename, size_t block_size) {
FILE *in_file = fopen(in_filename, "r");
if (in_file) {
FILE *out_file = fopen(out_filename, "w");
if (out_file) {
char *buffer = malloc(block_size);
if (buffer) {
int bytes_read = fread(buffer, 1, block_size, in_file);
if (bytes_read > 0) {
int bytes_written = fwrite(buffer, 1, bytes_read, out_file);
if (bytes_written == bytes_read) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Copy block completed successfully." object:nil];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to write to output file." object:nil];
}
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to read from input file." object:nil];
}
free(buffer);
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to allocate buffer." object:nil];
}
fclose(out_file);
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to open output file." object:nil];
}
fclose(in_file);
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to open input file." object:nil];
}
}
This leads to deeply nested "flock of geese" code that can be error
prone and hard to read. One technique to deal with this is to return
from the function when an error is encountered. The same code
implemented that way looks like this:
void copy_block(char const *in_filename, char const *out_filename, size_t block_size) {
FILE *in_file = fopen(in_filename, "r");
if ( ! in_file) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to open input file." object:nil];
return;
}
FILE *out_file = fopen(out_filename, "w");
if ( ! out_file) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to open output file." object:nil];
fclose(in_file); // clean up
return;
}
char *buffer = malloc(block_size);
if ( ! buffer) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to allocate buffer." object:nil];
fclose(out_file); // clean up
fclose(in_file);
return;
}
int bytes_read = fread(buffer, 1, block_size, in_file);
if (bytes_read <= 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to read from input file." object:nil];
free(buffer); // clean up
fclose(out_file);
fclose(in_file);
return;
}
int bytes_written = fwrite(buffer, 1, bytes_read, out_file);
if (bytes_written != bytes_read) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to write to output file." object:nil];
free(buffer); // clean up
fclose(out_file);
fclose(in_file);
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:@"Copy block completed successfully." object:nil];
// clean up
free(buffer);
fclose(out_file);
fclose(in_file);
}
One criticism of this approach is that clean up code is duplicated
repeatedly and in different variations, a violation of the
DRY
principle. Some programmers also prefer to have a single return
point in a function. Using goto
, you can centralize clean
up code in one place in the function (and as a side effect, the
function now has a single return point):
void copy_block(char const *in_filename, char const *out_filename, size_t block_size) {
FILE *in_file = fopen(in_filename, "r");
if ( ! in_file) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to open input file." object:nil];
goto end;
}
FILE *out_file = fopen(out_filename, "w");
if ( ! out_file) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to open output file." object:nil];
goto clean_up_in_file;
}
char *buffer = malloc(block_size);
if ( ! buffer) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to allocate buffer." object:nil];
goto clean_up_files;
}
int bytes_read = fread(buffer, 1, block_size, in_file);
if (bytes_read <= 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to read from input file." object:nil];
goto clean_up_all;
}
int bytes_written = fwrite(buffer, 1, bytes_read, out_file);
if (bytes_written != bytes_read) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Unable to write to output file." object:nil];
goto clean_up_all;
}
[[NSNotificationCenter defaultCenter] postNotificationName:@"Copy block completed successfully." object:nil];
// clean up
clean_up_all:
free(buffer);
clean_up_files:
fclose(out_file);
clean_up_in_file:
fclose(in_file);
end:
return;
}
Please note that this is not a recommendation to always structure your
error handling in this fashion using goto
. This is simply
one technique among many that you may encounter "in the wild" and which
you may choose to use in the appropriate situation. When
goto
is used carefully and sparingly, it can help make
difficult code cleaner and easier to follow, but unrestrained use of
goto
has the opposite effect. Whenever you're tempted to
use goto
in your own code, you should stop and see if you
can break the code down into smaller functions or methods. Very often,
refactoring a long function or method by extracting chunks of code into
smaller functions or methods will do far more for you than a
goto
can.
Next time, a
summary of
looping and a new topic:
variables.