我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
在Perl中,使用标签从循环中“goto”—使用“last”语句,这类似于break。
这样可以更好地控制嵌套循环。
也支持传统的goto标签,但我不确定是否有太多的实例,这是实现您想要的结果的唯一方法-子例程和循环应该足以满足大多数情况。
“goto”的问题和“无goto编程”运动最重要的论点是,如果你过于频繁地使用它,尽管你的代码可能表现正确,但它会变得不可读、不可维护、不可审查等。在99.99%的情况下,“goto”会导致意大利面条代码。就我个人而言,我想不出任何好的理由来解释为什么我会使用“goto”。
我们使用的goto规则是,goto可以跳转到函数中的单个退出清理点。在真正复杂的函数中,我们放松了这个规则,允许其他跳转。在这两种情况下,我们都避免了经常在错误代码检查中出现的深度嵌套的if语句,这有助于可读性和维护。
因为goto使得程序流的推理变得困难。“意大利面条代码”),goto通常只用于弥补缺失的功能:使用goto实际上可能是可以接受的,但前提是语言没有提供更结构化的变体来获得相同的目标。以《怀疑》为例:
我们使用的goto规则是,goto可以跳转到函数中的单个退出清理点。
这是对的——但前提是语言不允许使用清理代码进行结构化异常处理(如RAII或finally),后者可以更好地完成相同的工作(因为它是专门为此而构建的),或者有很好的理由不使用结构化异常处理(但除非在非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,goto唯一可接受的用法是退出嵌套循环。即使在这种情况下,将外部循环提升为自己的方法并使用return也总是更好。
除此之外,goto是对特定代码段考虑不够的标志。
支持goto实现一些限制的现代语言(例如,goto可能不会跳转到函数中或跳出函数),但问题从根本上还是一样的。
顺便说一句,其他语言特性当然也是如此,尤其是例外。而且通常有严格的规则,只在指定的地方使用这些特性,例如不使用异常来控制非异常程序流的规则。
下面是我所知道的使用“goto”语句的一些原因(有些人已经谈到了这个问题):
干净地退出函数
通常在一个函数中,您可能会分配资源并需要在多个位置退出。程序员可以通过将资源清理代码放在函数的末尾来简化他们的代码,并且函数的所有“出口点”都将进入清理标签。这样,您就不必在函数的每个“退出点”都编写清理代码。
退出嵌套循环
如果处于嵌套循环中,需要跳出所有循环,那么goto可以比break语句和If -checks更简洁。
低水平的性能改进
这只在对性能要求严格的代码中有效,但是goto语句执行得非常快,并且可以在遍历函数时提高性能。然而,这是一把双刃剑,因为编译器通常不能优化包含goto的代码。
注意,在所有这些示例中,gotos都被限制在单个函数的范围内。
在c#中,switch语句不允许切换。因此,goto用于将控制转移到特定的开关箱标签或默认标签。
例如:
switch(value)
{
case 0:
Console.WriteLine("In case 0");
goto case 1;
case 1:
Console.WriteLine("In case 1");
goto case 2;
case 2:
Console.WriteLine("In case 2");
goto default;
default:
Console.WriteLine("In default");
break;
}
编辑:有一个例外的“不掉落”规则。如果case语句没有代码,则允许延迟执行。
如果有,为什么?
C语言没有多级/标记的中断,并不是所有的控制流都可以用C语言的迭代和决策原语轻松建模。Gotos对纠正这些缺陷大有帮助。
有时使用某种类型的标志变量来实现一种伪多级中断更清晰,但它并不总是优于goto(至少goto可以轻松地确定控制的位置,不像标志变量),有时您只是不想为了避免goto而付出旗帜/其他扭曲的性能代价。
Libavcodec是一段性能敏感的代码。控制流的直接表达可能是优先考虑的,因为它往往会运行得更好。
goto不好的一个原因是,除了编码风格之外,你可以用它来创建重叠但非嵌套的循环:
loop1:
a
loop2:
b
if(cond1) goto loop1
c
if(cond2) goto loop2
这将创建一个奇怪的,但可能是合法的流控制结构,其中可能有(a, b, c, b, a, b, a, b, b,…)这样的序列,这让编译器黑客不高兴。显然,有许多聪明的优化技巧依赖于这种类型的结构不发生。(我应该检查一下我的龙书……)这样做的结果(使用一些编译器)可能是对包含gotos的代码没有进行其他优化。
如果你知道它只是“哦,顺便说一下”,恰好说服编译器发出更快的代码,那么它可能会很有用。就我个人而言,我更喜欢在使用像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等等——它们有合法的用途,但很容易被滥用。例如,在完全非异常的情况下,抛出异常纯粹是为了转义深度嵌套的控制结构。)
在Perl模块中,有时希望动态地创建子例程或闭包。问题是,一旦你创建了子例程,你如何得到它。你可以直接调用它,但是如果子例程使用caller(),那么它就没有那么有用了。这就是goto &子例程变化可能有用的地方。
这里有一个简单的例子:
sub AUTOLOAD{
my($self) = @_;
my $name = $AUTOLOAD;
$name =~ s/.*:://;
*{$name} = my($sub) = sub{
# the body of the closure
}
goto $sub;
# nothing after the goto will ever be executed.
}
您还可以使用这种形式的goto来提供尾部调用优化的基本形式。
sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
$tally *= $n--;
@_ = ($n,$tally);
goto &factorial;
}
(在Perl 5 version 16中,最好写成goto __SUB__;)
有一个模块会导入尾修饰符,如果你不喜欢使用这种形式的goto,还有一个模块会导入递归。
use Sub::Call::Tail;
sub AUTOLOAD {
...
tail &$sub( @_ );
}
use Sub::Call::Recur;
sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
recur( $n-1, $tally * $n );
}
使用goto的大多数其他原因都可以用其他关键字更好地完成。
比如重写一段代码:
LABEL: ;
...
goto LABEL if $x;
{
...
redo if $x;
}
或者从多个地方找到最后一段代码:
goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
last if $x;
...
last if $y
...
}
这些年来,我写了不少汇编语言。最终,每一种高级语言都被编译成gotos。好吧,叫它们“分支”或“跳跃”或其他什么,但它们是gotos。有人能写无goto汇编器吗?
当然,你可以向Fortran、C或BASIC程序员指出,gotos的泛滥就像意大利肉酱面一样。然而,答案不是避免它们,而是小心地使用它们。
刀可以用来准备食物,解救某人,或者杀死某人。我们会因为害怕后者而没有刀吗?同样,“后向”:不小心使用它会碍事,小心使用它会有所帮助。
盲目地遵循最佳实践并不是最佳实践。避免将goto语句作为流控制的主要形式是为了避免产生难以阅读的意大利面条代码。如果在正确的地方谨慎使用,它们有时可以成为表达想法的最简单、最清晰的方式。Walter Bright, Zortech c++编译器和D编程语言的创造者,经常使用它们,但很明智。即使使用了goto语句,他的代码仍然完全可读。
底线:为了避免去那里而避免去那里是毫无意义的。您真正需要避免的是生成不可读的代码。如果充满goto的代码是可读的,那么它就没有任何问题。
我发现有趣的是,有些人会给出一个可以接受goto的例子列表,说所有其他的用法都是不可接受的。你真的认为你知道每种情况下goto是表达算法的最佳选择吗?
为了说明这一点,我将给你一个还没有人展示过的例子:
今天我在写代码,在哈希表中插入一个元素。哈希表是以前计算的缓存,可以随意重写(影响性能但不影响正确性)。
哈希表的每个桶都有4个槽,当桶满时,我有一堆标准来决定覆盖哪个元素。现在,这意味着在一个桶中最多要经过三次,就像这样:
// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if (slot_p[add_index].hash_key == hash_key)
goto add;
// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
goto add;
// Additional passes go here...
add:
// element is written to the hash table here
如果不使用goto,代码会是什么样子?
就像这样:
// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if (slot_p[add_index].hash_key == hash_key)
break;
if (add_index >= ELEMENTS_PER_BUCKET) {
// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
break;
if (add_index >= ELEMENTS_PER_BUCKET)
// Additional passes go here (nested further)...
}
// element is written to the hash table here
如果添加更多的遍数,它看起来会越来越糟,而带有goto的版本始终保持相同的缩进级别,并避免使用虚假的if语句,其结果由前一个循环的执行暗示。
所以在另一种情况下,goto使代码更清晰,更容易编写和理解……我相信还有更多的例子,所以不要假装知道所有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
Everybody who is anti-goto cites, directly or indirectly, Edsger Dijkstra's GoTo Considered Harmful article to substantiate their position. Too bad Dijkstra's article has virtually nothing to do with the way goto statements are used these days and thus what the article says has little to no applicability to the modern programming scene. The goto-less meme verges now on a religion, right down to its scriptures dictated from on high, its high priests and the shunning (or worse) of perceived heretics.
让我们把Dijkstra的论文放在背景中,对这个问题有一些了解。
When Dijkstra wrote his paper the popular languages of the time were unstructured procedural ones like BASIC, FORTRAN (the earlier dialects) and various assembly languages. It was quite common for people using the higher-level languages to jump all over their code base in twisted, contorted threads of execution that gave rise to the term "spaghetti code". You can see this by hopping on over to the classic Trek game written by Mike Mayfield and trying to figure out how things work. Take a few moments to look that over.
这就是Dijkstra在1968年的论文中谴责的“毫无节制地使用go to语句”。这就是他所生活的环境,促使他写出了那篇论文。在你的代码中,在你喜欢的任何地方跳转的能力是他所批评和要求停止的。将其与C或其他更现代的语言中goto的弱功能进行比较简直是可笑的。
我已经能听到信徒们面对异教徒时扬起的呐喊声了。“但是,”他们会念叨,“用c语言的goto会让代码变得很难读。”哦,是吗?如果没有goto,代码也会变得难以阅读。比如这个:
#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
_-_-_-_
_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_
_-_-_-_
}
看不见goto,所以它一定很容易读,对吧?或者这个怎么样:
a[900]; b;c;d=1 ;e=1;f; g;h;O; main(k,
l)char* *l;{g= atoi(* ++l); for(k=
0;k*k< g;b=k ++>>1) ;for(h= 0;h*h<=
g;++h); --h;c=( (h+=g>h *(h+1)) -1)>>1;
while(d <=g){ ++O;for (f=0;f< O&&d<=g
;++f)a[ b<<5|c] =d++,b+= e;for( f=0;f<O
&&d<=g; ++f)a[b <<5|c]= d++,c+= e;e= -e
;}for(c =0;c<h; ++c){ for(b=0 ;b<k;++
b){if(b <k/2)a[ b<<5|c] ^=a[(k -(b+1))
<<5|c]^= a[b<<5 |c]^=a[ (k-(b+1 ))<<5|c]
;printf( a[b<<5|c ]?"%-4d" :" " ,a[b<<5
|c]);} putchar( '\n');}} /*Mike Laman*/
也不去那里。因此它必须是可读的。
我举这些例子的重点是什么?并不是语言特性导致代码不可读、不可维护。这不是语法造成的。这是糟糕的程序员造成的。而糟糕的程序员,正如你在上面的项目中所看到的,可以使任何语言特性无法阅读和使用。比如上面的for循环。(你能看到他们,对吧?)
公平地说,有些语言结构比其他结构更容易被滥用。然而,如果你是一个C程序员,我会更仔细地观察#define大约50%的使用情况,然后才会开始反对goto!
因此,对于那些已经阅读到这里的人来说,有几个关键点需要注意。
Dijkstra's paper on goto statements was written for a programming environment where goto was a lot more potentially damaging than it is in most modern languages that aren't an assembler. Automatically throwing away all uses of goto because of this is about as rational as saying "I tried to have fun once but didn't like it so now I'm against it". There are legitimate uses of the modern (anaemic) goto statements in code that cannot be adequately replaced by other constructs. There are, of course, illegitimate uses of the same statements. There are, too, illegitimate uses of the modern control statements like the "godo" abomination where an always-false do loop is broken out of using break in place of a goto. These are often worse than judicious use of goto.
我发现do{} while(false)的用法完全令人反感。它可能会让我相信它在某些奇怪的情况下是必要的,但从来没有让我相信它是干净合理的代码。
如果必须执行这样的循环,为什么不显式地依赖标志变量呢?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
当然,可以使用GOTO,但是有一件事比代码风格更重要,或者在使用它时,您必须考虑到代码是否可读:其中的代码可能不像您想象的那样健壮。
例如,看看下面的两个代码片段:
If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)
GOTO的等效代码
If A == 0 Then GOTO FINAL EndIf
A = 0
FINAL:
Write("Value of A:" + A)
我们首先想到的是这两段代码的结果将是“Value of A: 0”(当然,我们假设执行没有并行性)
这是不正确的:在第一个示例中,A将始终为0,但在第二个示例中(使用GOTO语句)A可能不是0。为什么?
原因是,从程序的另一点,我可以插入一个GOTO FINAL而不控制a的值。
这个例子非常明显,但是随着程序变得越来越复杂,看到这些东西的难度也增加了。
相关材料可以在Dijkstra先生的著名文章“反对GO TO声明的案例”中找到
我在以下情况下使用goto: 当需要从不同位置的函数返回时,并且在返回之前需要进行一些初始化:
non-goto版本:
int doSomething (struct my_complicated_stuff *ctx)
{
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
db_disconnect(conn);
return -1;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
free(temp_data);
db_disconnect(conn);
return -2;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -3;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -4;
}
if (ctx->something_else->additional_check) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -5;
}
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return 0;
}
goto版本:
int doSomething_goto (struct my_complicated_stuff *ctx)
{
int ret=0;
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
ret=-1;
goto exit_db;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
ret=-2;
goto exit_freetmp;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
ret=-3;
goto exit;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
ret=-4;
goto exit_freekey;
}
if (ctx->something_else->additional_check) {
ret=-5;
goto exit_freekey;
}
exit_freekey:
rsa_free(key);
exit:
pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
free(temp_data);
exit_db:
db_disconnect(conn);
return ret;
}
当您需要更改释放语句中的某些内容时(每个语句在代码中使用一次),第二个版本使它变得更容易,并减少了在添加新分支时跳过其中任何一个语句的机会。在函数中移动它们在这里不会有帮助,因为可以在不同的“级别”进行释放。
关于goto语句,它们的合法用途,以及可以用来代替“有道德的goto语句”但也可以像goto语句一样容易被滥用的替代结构,最深思熟虑和彻底的讨论是Donald Knuth的文章“使用goto语句的结构化编程”,在1974年12月的Computing Surveys(卷6,no. 1)中。4. 第261 - 301页)。
毫不奇怪,这篇39年前的论文的某些方面已经过时了:处理能力的数量级增长使得Knuth的一些性能改进对于中等规模的问题来说并不明显,从那时起就发明了新的编程语言结构。(例如,try-catch块包含Zahn的Construct,尽管它们很少以这种方式使用。)但Knuth涵盖了争论的方方面面,在任何人再次重复这个问题之前,都应该要求阅读。
看看在C语言中编程时何时使用Goto:
Although the use of goto is almost always bad programming practice (surely you can find a better way of doing XYZ), there are times when it really isn't a bad choice. Some might even argue that, when it is useful, it's the best choice. Most of what I have to say about goto really only applies to C. If you're using C++, there's no sound reason to use goto in place of exceptions. In C, however, you don't have the power of an exception handling mechanism, so if you want to separate out error handling from the rest of your program logic, and you want to avoid rewriting clean up code multiple times throughout your code, then goto can be a good choice.
我是什么意思?你可能会有这样的代码:
int big_function()
{
/* do some work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* clean up*/
return [success];
}
This is fine until you realize that you need to change your cleanup code. Then you have to go through and make 4 changes. Now, you might decide that you can just encapsulate all of the cleanup into a single function; that's not a bad idea. But it does mean that you'll need to be careful with pointers -- if you plan to free a pointer in your cleanup function, there's no way to set it to then point to NULL unless you pass in a pointer to a pointer. In a lot of cases, you won't be using that pointer again anyway, so that may not be a major concern. On the other hand, if you add in a new pointer, file handle, or other thing that needs cleanup, then you'll need to change your cleanup function again; and then you'll need to change the arguments to that function.
通过使用goto,它将是
int big_function()
{
int ret_val = [success];
/* do some work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
end:
/* clean up*/
return ret_val;
}
这样做的好处是,您的代码可以访问执行清理所需的所有内容,并且您已经成功地减少了更改点的数量。另一个好处是你的函数从多个出口点变成了只有一个;不可能不小心从函数返回而不进行清理。
此外,由于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,只要它们是向前的。
有些人说在c++中没有去的理由。有人说99%的情况下都有更好的选择。这不是推理,只是非理性的印象。下面是一个可靠的例子,goto会导致一个很好的代码,比如增强的do-while循环:
int i;
PROMPT_INSERT_NUMBER:
std::cout << "insert number: ";
std::cin >> i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'\n');
goto PROMPT_INSERT_NUMBER;
}
std::cout << "your number is " << i;
将其与goto-free代码进行比较:
int i;
bool loop;
do {
loop = false;
std::cout << "insert number: ";
std::cin >> i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'\n');
loop = true;
}
} while(loop);
std::cout << "your number is " << i;
我看到了这些差异:
需要嵌套的{}块(尽管do{…}而看起来更熟悉) 需要额外的循环变量,在四个地方使用 阅读和理解带有循环的工作需要更长的时间 循环不保存任何数据,它只是控制执行的流程,这比简单的标签更难理解
还有一个例子
void sort(int* array, int length) {
SORT:
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
goto SORT; // it is very easy to understand this code, right?
}
}
现在让我们摆脱“邪恶”的goto:
void sort(int* array, int length) {
bool seemslegit;
do {
seemslegit = true;
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
seemslegit = false;
}
} while(!seemslegit);
}
你看,这是使用goto的同一类型,它是结构良好的模式,它不像唯一推荐的方式那样转发goto。你肯定想避免这样的“智能”代码:
void sort(int* array, int length) {
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
i = -1; // it works, but WTF on the first glance
}
}
关键是goto很容易被误用,但goto本身不应该受到指责。注意,在c++中,label有函数作用域,所以它不会像纯汇编那样污染全局作用域,在纯汇编中,重叠循环有它的位置,而且非常常见——比如下面8051的代码,其中7段显示连接到P1。该程序循环闪电段周围:
; P1 states loops
; 11111110 <-
; 11111101 |
; 11111011 |
; 11110111 |
; 11101111 |
; 11011111 |
; |_________|
init_roll_state:
MOV P1,#11111110b
ACALL delay
next_roll_state:
MOV A,P1
RL A
MOV P1,A
ACALL delay
JNB P1.5, init_roll_state
SJMP next_roll_state
还有一个优点:goto可以作为命名循环、条件和其他流:
if(valid) {
do { // while(loop)
// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket
} while(loop);
} // if(valid)
或者你可以使用等效的goto和缩进,所以如果你明智地选择标签名称,你不需要注释:
if(!valid) goto NOTVALID;
LOOPBACK:
// more than one page of code here
if(loop) goto LOOPBACK;
NOTVALID:;
它有时在按字符进行字符串处理时很方便。
想象一下这样一个printf式的例子:
for cur_char, next_char in sliding_window(input_string) {
if cur_char == '%' {
if next_char == '%' {
cur_char_index += 1
goto handle_literal
}
# Some additional logic
if chars_should_be_handled_literally() {
goto handle_literal
}
# Handle the format
}
# some other control characters
else {
handle_literal:
# Complicated logic here
# Maybe it's writing to an array for some OpenGL calls later or something,
# all while modifying a bunch of local variables declared outside the loop
}
}
您可以将goto handle_literal重构为一个函数调用,但如果它修改了几个不同的局部变量,则必须将引用传递给每个局部变量,除非您的语言支持可变闭包。如果您的逻辑使else case不起作用,那么您仍然必须在调用之后使用continue语句(可以说是goto的一种形式)以获得相同的语义。
我还在lexer中明智地使用了gotos,通常用于类似的情况。大多数时候你不需要它们,但在一些奇怪的情况下有它们很好。
我遇到过goto是一个很好的解决方案的情况,我在这里或其他地方都没有看到过这个例子。
我有一个开关的情况下,所有的情况都需要调用相同的函数在最后。我有其他的情况,最后都需要调用不同的函数。
这看起来有点像这样:
switch( x ) {
case 1: case1() ; doStuffFor123() ; break ;
case 2: case2() ; doStuffFor123() ; break ;
case 3: case3() ; doStuffFor123() ; break ;
case 4: case4() ; doStuffFor456() ; break ;
case 5: case5() ; doStuffFor456() ; break ;
case 6: case6() ; doStuffFor456() ; break ;
case 7: case7() ; doStuffFor789() ; break ;
case 8: case8() ; doStuffFor789() ; break ;
case 9: case9() ; doStuffFor789() ; break ;
}
我没有给每个case一个函数调用,而是用goto代替了break。goto跳转到一个标签,这也是在开关的情况下。
switch( x ) {
case 1: case1() ; goto stuff123 ;
case 2: case2() ; goto stuff123 ;
case 3: case3() ; goto stuff123 ;
case 4: case4() ; goto stuff456 ;
case 5: case5() ; goto stuff456 ;
case 6: case6() ; goto stuff456 ;
case 7: case7() ; goto stuff789 ;
case 8: case8() ; goto stuff789 ;
case 9: case9() ; goto stuff789 ;
stuff123: doStuffFor123() ; break ;
stuff456: doStuffFor456() ; break ;
stuff789: doStuffFor789() ; break ;
}
案例1到3都必须调用doStuffFor123(),类似的案例4到6必须调用doStuffFor456()等。
在我看来,如果你正确使用它们,gotos是非常好的。最后,任何代码都像人们写的那样清晰。有了gotos,你可以写出意大利面代码,但这并不意味着gotos是意大利面代码的原因。这个事业就是我们;程序员。如果我愿意,我也可以用函数创建意大利面条代码。宏也是如此。