A
Arthur J. O'Dwyer
(This is a silly argument. Any bug in your code will "make a joke of"
the program's invariants, sure. But a well-used goto won't be any worse
than a well-used 'while' or 'if'. In fact, I'd argue that the average
'if' probably introduces more invariant-related bugs than the average
'goto', since the former introduces two new control paths while the
latter does not.
Operative word: "can." Not "must." I don't think anyone is suggesting
the use of pathological control flow, e.g.
if (x) goto a;
do {
while (y) {
a: ++foo;
}
if (z) goto a;
} while (w);
Sure, that's hard to reason about. (Incidentally, I have no idea whether
that program has an "irreducible flow graph." I think I get the general
idea of what makes a hairy flow graph, but I don't know how to define it
precisely.)
I can imagine that programs using /only/ gotos would be hard to
comprehend (e.g., I'd expect a 1975 study pitting Fortran against
Ratfor to find that Ratfor wins), but I disagree with the blanket
statement. Why, just yesterday I wrote
static char buffer[1000];
char *p, *pstart;
while (fgets(buffer, sizeof buffer, in) != NULL) {
for (p=buffer; isspace(*p); ++p) continue;
if (!strncmp(p, "%%MERR", 6)) goto all_done;
where the 'goto all_done' is exactly equivalent to a 'break'. But
the end of this loop was a screen and a half away, and there were
nested loops in between. So the mnemonic value of the 'all_done'
label far outweighed the benefits of using a 'break' there, whatever
those benefits might have been.
There's another 'goto' in that same program, this time branching
back to the top of a loop but after the condition has been executed,
like this (simplified drastically):
while (fgets(buf, sizeof buf, fp) != NULL) {
top_of_loop:
process(buf);
if (buffered_input_remains) {
strcpy(buf, buffered_line());
goto top_of_loop;
}
}
This paragraph sounds much more reasonable to me.
-Arthur
the program's invariants, sure. But a well-used goto won't be any worse
than a well-used 'while' or 'if'. In fact, I'd argue that the average
'if' probably introduces more invariant-related bugs than the average
'goto', since the former introduces two new control paths while the
latter does not.
The tone here is that the effects of using 'goto' aren't that much
different from using 'break' or (presumably early) 'return'. That's
not so, for both a theoretical reason and a practical reason.
The theoretical reason is that 'goto' can lead to irreducible flow
graphs. Early returns or using 'break' to exit a loop (or a 'switch')
cannot cause irreducible flow graphs. Irreducible flows are more
difficult to reason about than reducible flow graphs.
Operative word: "can." Not "must." I don't think anyone is suggesting
the use of pathological control flow, e.g.
if (x) goto a;
do {
while (y) {
a: ++foo;
}
if (z) goto a;
} while (w);
Sure, that's hard to reason about. (Incidentally, I have no idea whether
that program has an "irreducible flow graph." I think I get the general
idea of what makes a hairy flow graph, but I don't know how to define it
precisely.)
The practical reason is that programs using 'goto' are harder to
comprehend than programs using early returns or loop exits. At least,
that was the conclusion of studies that were done looking at how the
different control mechanisms affected programmer performance. (Sorry
I don't have a citation handy; I think it was in Software Practice
and Experience, sometime in the early 1980's, or maybe late 1970's.)
I can imagine that programs using /only/ gotos would be hard to
comprehend (e.g., I'd expect a 1975 study pitting Fortran against
Ratfor to find that Ratfor wins), but I disagree with the blanket
statement. Why, just yesterday I wrote
static char buffer[1000];
char *p, *pstart;
while (fgets(buffer, sizeof buffer, in) != NULL) {
for (p=buffer; isspace(*p); ++p) continue;
if (!strncmp(p, "%%MERR", 6)) goto all_done;
where the 'goto all_done' is exactly equivalent to a 'break'. But
the end of this loop was a screen and a half away, and there were
nested loops in between. So the mnemonic value of the 'all_done'
label far outweighed the benefits of using a 'break' there, whatever
those benefits might have been.
There's another 'goto' in that same program, this time branching
back to the top of a loop but after the condition has been executed,
like this (simplified drastically):
while (fgets(buf, sizeof buf, fp) != NULL) {
top_of_loop:
process(buf);
if (buffered_input_remains) {
strcpy(buf, buffered_line());
goto top_of_loop;
}
}
My own (informal) rules for goto are: (1) only forward, never
backward, (2) never into a nested scope, and (3) target label
at the outermost level in a function body. (Yes I know 2 and
3 are not independent. Furthermore, it doesn't come up very
often - the most common case for code that I write is for whole
files to have not a single 'goto' in them. At the other end
of the scale, I would guess files with early returns happen
more often than not, and occurrences of 'break' being somewhere
in the middle.
This paragraph sounds much more reasonable to me.
-Arthur