我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
当前回答
# ifdef TONGUE_IN_CHEEK
Perl有一个goto,它允许您实现穷人的尾部调用。: - p
sub factorial {
my ($n, $acc) = (@_, 1);
return $acc if $n < 1;
@_ = ($n - 1, $acc * $n);
goto &factorial;
}
# endif
好吧,所以这和C的goto没有关系。更重要的是,我同意其他关于使用goto进行清理或实现Duff的设备等的评论。这都是关于利用,而不是滥用。
(同样的注释可以应用于longjmp、异常、call/cc等等——它们有合法的用途,但很容易被滥用。例如,在完全非异常的情况下,抛出异常纯粹是为了转义深度嵌套的控制结构。)
其他回答
如果有,为什么?
C语言没有多级/标记的中断,并不是所有的控制流都可以用C语言的迭代和决策原语轻松建模。Gotos对纠正这些缺陷大有帮助。
有时使用某种类型的标志变量来实现一种伪多级中断更清晰,但它并不总是优于goto(至少goto可以轻松地确定控制的位置,不像标志变量),有时您只是不想为了避免goto而付出旗帜/其他扭曲的性能代价。
Libavcodec是一段性能敏感的代码。控制流的直接表达可能是优先考虑的,因为它往往会运行得更好。
因为goto使得程序流的推理变得困难。“意大利面条代码”),goto通常只用于弥补缺失的功能:使用goto实际上可能是可以接受的,但前提是语言没有提供更结构化的变体来获得相同的目标。以《怀疑》为例:
我们使用的goto规则是,goto可以跳转到函数中的单个退出清理点。
这是对的——但前提是语言不允许使用清理代码进行结构化异常处理(如RAII或finally),后者可以更好地完成相同的工作(因为它是专门为此而构建的),或者有很好的理由不使用结构化异常处理(但除非在非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,goto唯一可接受的用法是退出嵌套循环。即使在这种情况下,将外部循环提升为自己的方法并使用return也总是更好。
除此之外,goto是对特定代码段考虑不够的标志。
支持goto实现一些限制的现代语言(例如,goto可能不会跳转到函数中或跳出函数),但问题从根本上还是一样的。
顺便说一句,其他语言特性当然也是如此,尤其是例外。而且通常有严格的规则,只在指定的地方使用这些特性,例如不使用异常来控制非异常程序流的规则。
1) The most common use of goto that I know of is emulating exception handling in languages that don't offer it, namely in C. (The code given by Nuclear above is just that.) Look at the Linux source code and you'll see a bazillion gotos used that way; there were about 100,000 gotos in Linux code according to a quick survey conducted in 2013: http://blog.regehr.org/archives/894. Goto usage is even mentioned in the Linux coding style guide: https://www.kernel.org/doc/Documentation/CodingStyle. Just like object-oriented programming is emulated using structs populated with function pointers, goto has its place in C programming. So who is right: Dijkstra or Linus (and all Linux kernel coders)? It's theory vs. practice basically.
There is however the usual gotcha for not having compiler-level support and checks for common constructs/patterns: it's easier to use them wrong and introduce bugs without compile-time checks. Windows and Visual C++ but in C mode offer exception handling via SEH/VEH for this very reason: exceptions are useful even outside OOP languages, i.e. in a procedural language. But the compiler can't always save your bacon, even if it offers syntactic support for exceptions in the language. Consider as example of the latter case the famous Apple SSL "goto fail" bug, which just duplicated one goto with disastrous consequences (https://www.imperialviolet.org/2014/02/22/applebug.html):
if (something())
goto fail;
goto fail; // copypasta bug
printf("Never reached\n");
fail:
// control jumps here
使用编译器支持的异常也会出现同样的错误,例如在c++中:
struct Fail {};
try {
if (something())
throw Fail();
throw Fail(); // copypasta bug
printf("Never reached\n");
}
catch (Fail&) {
// control jumps here
}
But both variants of the bug can be avoided if the compiler analyzes and warns you about unreachable code. For example compiling with Visual C++ at the /W4 warning level finds the bug in both cases. Java for instance forbids unreachable code (where it can find it!) for a pretty good reason: it's likely to be a bug in the average Joe's code. As long as the goto construct doesn't allow targets that the compiler can't easily figure out, like gotos to computed addresses(**), it's not any harder for the compiler to find unreachable code inside a function with gotos than using Dijkstra-approved code.
(**) Footnote: Gotos to computed line numbers are possible in some versions of Basic, e.g. GOTO 10*x where x is a variable. Rather confusingly, in Fortran "computed goto" refers to a construct that is equivalent to a switch statement in C. Standard C doesn't allow computed gotos in the language, but only gotos to statically/syntactically declared labels. GNU C however has an extension to get the address of a label (the unary, prefix && operator) and also allows a goto to a variable of type void*. See https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html for more on this obscure sub-topic. The rest of this post ins't concerned with that obscure GNU C feature.
标准C(即未计算的)goto通常不是无法在编译时找到不可达代码的原因。通常的原因是如下所示的逻辑代码。鉴于
int computation1() {
return 1;
}
int computation2() {
return computation1();
}
对于编译器来说,在以下3种结构中找到不可访问的代码同样困难:
void tough1() {
if (computation1() != computation2())
printf("Unreachable\n");
}
void tough2() {
if (computation1() == computation2())
goto out;
printf("Unreachable\n");
out:;
}
struct Out{};
void tough3() {
try {
if (computation1() == computation2())
throw Out();
printf("Unreachable\n");
}
catch (Out&) {
}
}
(请原谅我使用了与大括号相关的编码风格,但我试图使示例尽可能紧凑。)
Visual c++ /W4(即使使用/Ox)也无法在这些类型中找到无法到达的代码,而且正如您可能知道的那样,寻找无法到达的代码的问题通常是无法确定的。(如果你不相信我的话:https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)
As a related issue, the C goto can be used to emulate exceptions only inside the body of a function. The standard C library offers a setjmp() and longjmp() pair of functions for emulating non-local exits/exceptions, but those have some serious drawbacks compared to what other languages offer. The Wikipedia article http://en.wikipedia.org/wiki/Setjmp.h explains fairly well this latter issue. This function pair also works on Windows (http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx), but hardly anyone uses them there because SEH/VEH is superior. Even on Unix, I think setjmp and longjmp are very seldom used.
2) I think the second most common use of goto in C is implementing multi-level break or multi-level continue, which is also a fairly uncontroversial use case. Recall that Java doesn't allow goto label, but allows break label or continue label. According to http://www.oracle.com/technetwork/java/simple-142616.html, this is actually the most common use case of gotos in C (90% they say), but in my subjective experience, system code tends to use gotos for error handling more often. Perhaps in scientific code or where the OS offers exception handling (Windows) then multi-level exits are the dominant use case. They don't really give any details as to the context of their survey.
编辑补充:这两种使用模式出现在Kernighan和Ritchie的C语言书的第60页左右(取决于版本)。另一件值得注意的事情是,这两个用例都只涉及forward goto。MISRA C 2012版(不像2004版)现在允许goto,只要它们是向前的。
嗯,有一件事总是比goto更糟糕;奇怪地使用其他程序流操作符来避免goto:
例子:
// 1
try{
...
throw NoErrorException;
...
} catch (const NoErrorException& noe){
// This is the worst
}
// 2
do {
...break;
...break;
} while (false);
// 3
for(int i = 0;...) {
bool restartOuter = false;
for (int j = 0;...) {
if (...)
restartOuter = true;
if (restartOuter) {
i = -1;
}
}
etc
etc
计算机科学家Edsger Dijkstra在该领域做出了重大贡献,他也因批评GoTo的使用而闻名。 维基百科上有一篇关于他观点的短文。