长期以来,我一直认为,如果可能,永远不要使用goto。 前几天在阅读libavcodec(用C编写)时,我注意到它的多种用法。 在支持循环和功能的语言中使用goto是否有优势? 如果是这样,为什么?
每个反对goto的人都直接或间接引用Edsger Dijkstra的《 GoTo被认为有害》一文,以证实自己的立场。太糟糕了,Dijkstra的文章实际上与这些天使用goto语句的方式无关,因此该文章所说的内容几乎不适用于现代编程领域。现在,goto较少的模因在宗教上趋于成真,直到其经文由高,大祭司以及被察觉的(或什至更糟的)异教徒所决定。
让我们将Dijkstra的论文放在上下文中,以阐明该主题。
当Dijkstra撰写论文时,当时流行的语言是非结构化的过程语言,例如BASIC,FORTRAN(较早的方言)和各种汇编语言。使用高级语言的人们在扭曲的,扭曲的执行线程中跳过整个代码库是很普遍的,从而产生了"意大利面条式代码"一词。您可以跳到迈克·梅菲尔德(Mike Mayfield)编写的经典《迷航(Trek)》游戏,并尝试弄清事情的发展过程,从而看到这一点。花点时间看一下。
这是Dijkstra在1968年的论文中所指责的"毫无限制地使用go to statement"。这是他所居住的环境促使他撰写该论文。他在批评中要求停止在代码中任意位置随意跳转的能力。将其与C或其他此类更现代的语言中goto的无穷大能力进行比较很简单。
当异教徒面对异端时,我已经能听到他们的吟唱。他们喊道:"但是,使用C语言中的goto会使代码很难阅读。"哦耶?如果没有goto,也会使代码很难阅读。像这个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f
",4.*-F/OO/OO);}F_OO()
{
_-_-_-_
_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_
_-_-_-_
} |
视线中没有goto,因此它必须易于阅读,对吗?还是这个呢?
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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( '
');}} /*Mike Laman*/ |
那里也没有goto。因此,它必须可读。
这些例子对我有什么意义?不是语言功能会导致无法阅读,无法维护的代码。不是语法可以做到这一点。造成这种情况的是不良的程序员。就像您在上面的项目中看到的那样,不良的程序员可能会使任何语言功能都不可读也不可用。就像for在那里循环一样。 (您可以看到它们,对吗?)
公平地说,某些语言结构比其他语言结构更容易滥用。但是,如果您是C程序员,那么在对goto进行深入研究之前,我会更密切地凝视#define的大约50%的使用!
因此,对于那些不愿阅读本文的人来说,有几点要注意。
Dijkstra关于goto语句的论文是为goto较多的编程环境编写的
比大多数不是汇编语言的现代语言更具潜在的破坏性。
因此,自动丢弃goto的所有用法就像说"我尝试过
曾经有一次乐趣,但不喜欢它,所以现在我反对它。"
代码中现代(厌食)goto语句的合法使用不能充分
被其他结构取代。
当然,对这些声明有非法使用。
现代控制语句也有非法使用,例如" godo"可憎性,其中总是使用break代替goto来打破始终为假的do循环。这些通常比明智地使用goto更糟糕。
我知道有一些使用" goto"语句的原因(有些已经讲过):
干净地退出功能
通常在函数中,您可能会分配资源,并且需要在多个位置退出。程序员可以通过将资源清除代码放在函数的末尾来简化其代码,并且该函数的所有"退出点"都将进入清除标签。这样,您不必在函数的每个"退出点"都编写清除代码。
退出嵌套循环
如果您处于嵌套循环中并且需要脱离所有循环,那么goto可以比break语句和if-checks更加简洁明了。
低级别的性能改进
这仅在性能至关重要的代码中有效,但是goto语句执行得非常快,并且在遍历函数时可以助您一臂之力。但是,这是一把双刃剑,因为编译器通常无法优化包含gotos的代码。
请注意,在所有这些示例中,gotos仅限于单个函数的范围。
盲目遵守最佳实践不是最佳实践。避免将goto语句作为流控制的主要形式的想法是避免生成不可读的意大利面条代码。如果在正确的地方少用,它们有时可能是表达想法的最简单,最清晰的方法。 Zortech C ++编译器和D编程语言的创建者Walter Bright经常但明智地使用它们。即使使用goto语句,他的代码仍可完美阅读。
底线:为避免goto而避免goto是没有意义的。您真正要避免的是生成无法读取的代码。如果您的goto装载代码是可读的,则没有任何问题。
由于goto使对程序流的推理变得困难1(又称"意大利面条代码"),因此goto通常仅用于补偿缺少的功能:goto的使用实际上是可以接受的,但前提是该语言不支持。提供结构更复杂的变体以获得相同的目标。以Doubt为例:
The rule with goto that we use is that goto is okay to for jumping forward to a single exit cleanup point in a function.
这是正确的-但前提是该语言不允许使用清理代码(例如RAII或finally)来进行结构化异常处理,这样做的效果更好(因为它是专门为此目的而构建的),或者不采用结构化异常处理的充分理由(但除非是非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,goto唯一可接受的用法是退出嵌套循环。甚至在那里,将外部循环提升为自己的方法并改为使用return几乎总是更好的选择。
除此之外,goto表示没有对特定代码进行足够的思考。
1支持goto的现代语言实现了一些限制(例如,goto可能不会跳入或跳出函数),但问题基本上仍然相同。
顺便说一句,其他语言功能当然也是如此,最明显的例外是。而且通常有严格的规则来仅在指示的地方使用这些功能,例如不使用异常来控制非异常程序流的规则。
好吧,有一件事总是比goto's更糟糕。其他程序流运算符的奇怪用法,以避免转到:
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| // 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 |
在C#中,switch语句不允许掉线。因此goto用于将控制权转移到特定的开关箱标签或默认标签。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| switch(value)
{
case 0:
Console.Writeln("In case 0");
goto case 1;
case 1:
Console.Writeln("In case 1");
goto case 2;
case 2:
Console.Writeln("In case 2");
goto default;
default:
Console.Writeln("In default");
break;
} |
编辑:"不掉线"规则有一个例外。如果case语句没有代码,则允许穿透。
#ifdef TONGUE_IN_CHEEK
Perl有一个goto,允许您实现穷人的尾声。 :-P
1 2 3 4 5 6
| 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等),它们具有合法用途,但很容易被滥用。例如,抛出异常纯粹是为了逃避深层嵌套的控制结构,在完全非例外的情况下。)
这些年来,我已经编写了多行汇编语言。最终,每种高级语言都可以编译成gotos。好吧,称它们为"分支"或"跳跃"或其他名称,但它们是傻瓜。任何人都可以编写goto-less汇编程序吗?
现在确定,您可以指出一个Fortran,C或BASIC程序员,使用gotos进行暴动是意大利面条意粉的秘诀。但是,答案不是避免它们,而是要谨慎使用它们。
一把刀可以用来准备食物,释放某人或杀死某人。我们是否会因为担心刀而没有刀?同样,goto:粗心使用会阻碍,谨慎使用会有所帮助。
看看在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.
我什么意思您可能有一些类似以下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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];
} |
这很好,直到您意识到需要更改清除代码。然后,您必须进行4个更改。现在,您可能决定只将所有清理封装到一个函数中;这不是一个坏主意。但这确实意味着您需要小心使用指针-如果您打算在清理函数中释放指针,除非您将指针传递给指针,否则无法将其设置为指向NULL。在许多情况下,无论如何您都不会再次使用该指针,因此这可能不是主要问题。另一方面,如果添加了新的指针,文件句柄或其他需要清除的内容,则需要再次更改清除功能;然后您需要将参数更改为该函数。
通过使用goto,它将是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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应该始终谨慎使用,并且作为最后的手段-但是有时间和地方。问题不应该是"您是否必须使用它",而应该是"使用它是否是最佳选择"。
goto不好的原因之一,除了编码风格之外,您还可以使用它来创建重叠但非嵌套的循环:
1 2 3 4 5 6 7
| loop1:
a
loop2:
b
if(cond1) goto loop1
c
if(cond2) goto loop2 |
这将创建一种奇怪但可能合法的控制流程结构,其中可能有类似(a,b,c,b,a,b,a,b,...)的序列,这使编译器黑客感到不满。显然,有许多聪明的优化技巧依赖于这种类型的结构而不会发生。 (我应该检查我的龙书的副本……)(使用某些编译器)的结果可能是未对包含goto的代码进行其他优化。
如果您只是说"哦,顺便说一句",以说服编译器发出更快的代码,这可能会很有用。就个人而言,在使用goto之类的技巧之前,我更愿意尝试向编译器解释什么是可能的,什么不是,但是可以说,在破解汇编程序之前,我也可以尝试goto。
我觉得有些人会列举goto可以接受的情况,说所有其他用途都是不可接受的,这很有趣。您是否真的认为您知道goto是表示算法的最佳选择的每种情况?
为了说明,我将给您一个示例,这里没有人显示:
今天,我正在编写用于在哈希表中插入元素的代码。哈希表是先前计算的缓存,可以随意覆盖(影响性能,但不影响正确性)。
哈希表的每个存储区都有4个插槽,我有很多条件来决定存储区已满时要覆盖哪个元素。现在,这意味着最多要通过一个存储桶进行三遍,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // 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,那么这段代码会是什么样?
像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // 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可以跳转到函数中的单个出口清理点。在真正复杂的功能中,我们放宽该规则以允许其他跳转。在这两种情况下,我们都避免在错误代码检查中经常出现的嵌套if语句,这有助于提高可读性和可维护性。
唐纳德·努斯(Donald Knuth)的文章"使用goto语句进行结构化编程"是对goto语句,它们的合法使用以及可用于替代"虚拟goto语句"但可以像goto语句一样容易使用的替代结构的最周到,最彻底的讨论。 ,在1974年12月的计算机调查中(第6卷第4期,第261-301页)。
毫不奇怪,这份已有39年历史的论文的某些方面是过时的:处理能力的数量级增加使得Knuth的性能改进在中等大小的问题上不明显,并且从那时起发明了新的编程语言结构。 (例如,try-catch块包含Zahn的Construct,尽管很少以这种方式使用。)但是Knuth涵盖了论点的方方面面,在任何人再次讨论该问题之前,都应阅读该书。
在Perl模块中,您偶尔需要动态创建子例程或闭包。问题是,一旦创建了子例程,您将如何获得它。您可以调用它,但是如果子例程使用caller(),它将不会像它可能那样有用。这就是goto &subroutine变体可以提供帮助的地方。
这是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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的这种形式提供尾调用优化的基本形式。
1 2 3 4 5 6 7 8 9
| sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
$tally *= $n--;
@_ = ($n,$tally);
goto &factorial;
} |
(在Perl 5版本16中,最好将其写为goto __SUB__;)
如果您不喜欢使用这种形式的goto,则有一个模块将导入tail修饰符,而一个模块将导入recur。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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的大多数其他原因最好与其他关键字一起使用。
像redo那样写一些代码:
1 2 3
| LABEL: ;
...
goto LABEL if $x; |
或从多个位置转到代码的last:
1 2 3 4 5
| goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ; |
1 2 3 4 5 6
| {
last if $x;
...
last if $y
...
} |
我发现do {} while(false)用法完全令人反感。可以想到的是,在某些奇怪的情况下,我有必要说服我这样做,但是从来没有说过它是干净易懂的代码。
如果必须执行这样的循环,为什么不明确声明对flag变量的依赖性?
1
| for (stepfailed=0 ; ! stepfailed ; /*empty*/) |
同样没有人实现过" COME FROM"语句。
If so, why?
C没有多级/标记中断,并且并非所有控制流都可以使用C的迭代和决策原语轻松建模。 Goto在解决这些缺陷方面大有帮助。
有时使用某种标志变量来实现某种伪多级中断更为清晰,但它并不总是优于goto(至少goto允许一个人轻松确定控制权去向何处,这与flag变量不同),有时您根本不想支付标志/其他扭曲的性能价格来避免使用goto。
libavcodec是对性能敏感的代码。直接表达控制流可能是一个优先事项,因为它会更好地运行。
1)我所知道的goto最常见的用法是使用不提供它的语言(即C)来模拟异常处理。(上面Nuclear给出的代码就是这样。)看一下Linux源代码,您会发现我会看到这样使用了不计其数的硬币;根据2013年进行的一项快速调查:http://blog.regehr.org/archives/894,Linux代码中大约有100,000个goto。 Linux编码样式指南中甚至提到了Goto用法:https://www.kernel.org/doc/Documentation/CodingStyle。就像使用填充有函数指针的结构来模拟面向对象的编程一样,goto在C编程中也占有一席之地。那么谁是对的:Dijkstra或Linus(以及所有Linux内核编码器)?基本上是理论与实践。
好。
但是,通常的陷阱是没有编译器级别的支持,也没有检查常见的结构/模式:更容易错误地使用它们并引入错误,而无需进行编译时检查。出于这个原因,Windows和Visual C ++但在C模式下通过SEH / VEH提供异常处理:即使在OOP语言之外(即在过程语言中),异常也很有用。但是,即使编译器为语言中的异常提供了语法支持,也无法始终保存您的培根。以后一种情况为例,著名的Apple SSL" goto失败"错误仅复制了一个goto并带来了灾难性的后果(https://www.imperialviolet.org/2014/02/22/applebug.html):
好。
1 2 3 4 5 6 7
| if (something())
goto fail;
goto fail; // copypasta bug
printf("Never reached
");
fail:
// control jumps here |
使用编译器支持的异常,例如,您可能会遇到完全相同的错误。在C ++中:
好。
1 2 3 4 5 6 7 8 9 10 11 12
| struct Fail {};
try {
if (something())
throw Fail();
throw Fail(); // copypasta bug
printf("Never reached
");
}
catch (Fail&) {
// control jumps here
} |
但是,如果编译器分析并警告您无法访问的代码,则可以避免两种错误??。例如,在/ W4警告级别使用Visual C ++进行编译在两种情况下都会发现该错误。例如,Java出于一个很好的理由禁止无法访问的代码(可以在其中找到它!):它可能是普通Joe的代码中的错误。只要goto构造不允许编译器无法轻易找出的目标(例如将goto转换为计算地址(**)),对于使用gotos的函数在编译器中查找无法访问的代码,使用Dijkstra编译器就不会困难得多。批准的代码。
好。
(**)脚注:在某些基本版本中,可以转到计算的行号,例如GOTO 10 * x其中x是变量。令人困惑的是,在Fortran中," compute goto"是指等效于C中的switch语句的构造。标准C不允许使用该语言中的已计算的goto,而仅允许goto静态/语法声明的标签。但是,GNU C进行了扩展以获取标签的地址(一元,前缀&&运算符),并且还允许转到类型为void *的变量。有关这个晦涩的子主题的更多信息,请参见https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html。这篇文章的其余部分与那个晦涩的GNU C功能无关。
好。
标准C(即未计算)的指令通常不是在编译时找不到无法到达的代码的原因。通常的原因是如下所示的逻辑代码。给定
好。
1 2 3 4 5 6 7
| int computation1() {
return 1;
}
int computation2() {
return computation1();
} |
对于编译器来说,在以下3种构造中都找不到不可达的代码同样困难:
好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void tough1() {
if (computation1() != computation2())
printf("Unreachable
");
}
void tough2() {
if (computation1() == computation2())
goto out;
printf("Unreachable
");
out:;
}
struct Out{};
void tough3() {
try {
if (computation1() == computation2())
throw Out();
printf("Unreachable
");
}
catch (Out&) {
}
} |
(请原谅我与花括号相关的编码样式,但我尝试使这些示例尽可能紧凑。)
好。
Visual C ++ / W4(即使使用/ Ox)也无法在其中任何一个中找到无法访问的代码,并且您可能已经知道,通常无法确定查找无法访问的代码的问题。 (如果您不相信我的话:https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)
好。
作为一个相关问题,C goto只能用于在函数体内模拟异常。标准C库提供了setjmp()和longjmp()对函数来模拟非本地出口/异常,但是与其他语言相比,它们具有一些严重的缺点。 Wikipedia文章http://en.wikipedia.org/wiki/Setjmp.h很好地解释了后一个问题。该功能对在Windows(http://msdn.microsoft.com/zh-cn/library/yz2ez4as.aspx)上也可以使用,但是几乎没有人在此使用它们,因为SEH / VEH更为出色。即使在Unix上,我也认为很少使用setjmp和longjmp。
好。
2)我认为C语言中goto的第二个最常见用法是实现多级中断或多级继续,这也是一个毫无争议的用例。回想一下Java不允许goto标签,但是允许break标签或Continue标签。根据http://www.oracle.com/technetwork/java/simple-142616.html的介绍,这实际上是C语言中getos的最常见用例(他们说90%),但是根据我的主观经验,系统代码倾向于经常使用gotos进行错误处理。也许在科学代码中或OS提供异常处理(Windows)的地方,多级退出是主要的用例。他们并没有提供有关调查背景的任何细节。
好。
编辑添加:事实证明,这两种使用模式在第60页左右的Kernighan和Ritchie的C书中找到(取决于版本)。值得注意的另一件事是,两个用例都只涉及正向指令。事实证明,MISRA C 2012版本(与2004年版本不同)现在允许gotos,只要它们只是向前的即可。
好。
好。
有人说在C ++中没有理由进行goto。有人说在99%的情况下有更好的选择。这不是推理,只是不合理的印象。这是一个可靠的示例,其中goto导致了一个不错的代码,类似于增强的do-while循环:
1 2 3 4 5 6 7 8 9 10 11 12 13
| int i;
PROMPT_INSERT_NUMBER:
std::coutp;ltp;lt;"insert number:";
std::cinp;gtp;gt; i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'
');
goto PROMPT_INSERT_NUMBER;
}
std::coutp;ltp;lt;"your number is"p;ltp;lt; i; |
将其与免费代码进行比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int i;
bool loop;
do {
loop = false;
std::coutp;ltp;lt;"insert number:";
std::cinp;gtp;gt; i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'
');
loop = true;
}
} while(loop);
std::coutp;ltp;lt;"your number is"p;ltp;lt; i; |
我看到了这些差异:
-
需要嵌套的{}块(尽管do {...} while看起来更熟悉)
-
需要额外的loop变量,在四个地方使用
-
loop花费较长的时间阅读和理解作品
-
loop不保存任何数据,它仅控制执行的流程,比简单的标签更难理解
还有另一个例子
1 2 3 4 5 6 7
| void sort(int* array, int length) {
SORT:
for(int i=0; p;lt;length-1; ++i) if(array[ip;gt;array[i+1]) {
swap(data[i], data[i+1]);
goto SORT; // it is very easy to understand this code, right?
}
} |
现在让我们摆脱"邪恶的" goto:
1 2 3 4 5 6 7 8 9 10
| void sort(int* array, int length) {
bool seemslegit;
do {
seemslegit = true;
for(int i=0; p;lt;length-1; ++i) if(array[ip;gt;array[i+1]) {
swap(data[i], data[i+1]);
seemslegit = false;
}
} while(!seemslegit);
} |
您会发现它是使用goto的相同类型,它的结构合理,并且不像推荐的唯一方法那样前进goto。您肯定要避免这样的"智能"代码:
1 2 3 4 5 6
| void sort(int* array, int length) {
for(int i=0; p;lt;length-1; ++i) if(array[ip;gt;array[i+1]) {
swap(data[i], data[i+1]);
i = -1; // it works, but WTF on the first glance
}
} |
关键是goto容易被滥用,但是goto本身不应该受到指责。请注意,label在C ++中具有函数作用域,因此它不会像在纯汇编中那样污染全局范围,在纯汇编中重叠循环有其位置,并且非常常见-就像在8051的以下代码中,将7段显示器连接到P1。该程序在以下位置循环闪电段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ; P1 states loops
; 11111110p;lt;-
; 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可以用作命名循环,条件和其他流:
1 2 3 4 5 6 7 8 9
| 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与缩进一起使用,因此如果明智地选择标签名称,则无需注释:
1 2 3 4 5 6 7
| if(!valid) goto NOTVALID;
LOOPBACK:
// more than one page of code here
if(loop) goto LOOPBACK;
NOTVALID:; |
在Perl中,使用标签从循环中"转到"-使用" last"语句,这与break相似。
这样可以更好地控制嵌套循环。
也支持传统的goto标签,但我不确定有太多实例是这是实现所需功能的唯一方法-子例程和循环在大多数情况下就足够了。
我在以下情况下使用goto:
在需要从不同位置的函数返回时以及返回之前需要进行一些未初始化的操作:
非goto版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| 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;
} |
转到版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 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,但是有比代码样式更重要的一件事,或者在使用时必须牢记代码是否可读:内部代码可能不如您健壮认为。
例如,查看以下两个代码段:
1 2
| If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A) |
与GOTO等效的代码
1 2 3 4
| If A == 0 Then GOTO FINAL EndIf
A = 0
FINAL:
Write("Value of A:" + A) |
我们认为的第一件事是,代码的两个位的结果将是" A的值:0"(当然,我们假设执行时没有并行性)
这是不正确的:在第一个示例中,A将始终为0,但是在第二个示例中(使用GOTO语句),A可能不会为0。为什么?
原因是因为从程序的另一个角度来看,我可以插入GOTO FINAL而不控制A的值。
这个例子非常明显,但是随着程序变得越来越复杂,看到这类东西的难度也会增加。
相关材料可以在迪克斯特拉先生的著名文章"反对GO TO声明的案例"中找到。
" goto"和" goto-less编程"运动的最重要论据的问题是,如果使用得太频繁,您的代码虽然可能会正确运行,但变得难以阅读,无法维护,无法查看等。在99.99%的案件"转到"会导致意大利面条式代码。就我个人而言,我不能想到有什么很好的理由来说明为什么要使用" goto"。
它对于不时进行字符字符串处理非常有用。
试想像这样的printf式示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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重构为函数调用,但是如果要修改几个不同的局部变量,则除非您的语言支持可变闭包,否则您必须传递对每个局部变量的引用。 如果逻辑使其他情况不起作用,则在调用后仍必须使用continue语句(可以说是goto的一种形式)以获取相同的语义。
在词法分析器中,我也明智地使用了goto,通常用于类似情况。 您通常不需要它们,但是对于那些奇怪的情况,它们很好。
在该领域做出了重大贡献的计算机科学家Edsger Dijkstra也因批评GoTo的使用而闻名。
关于Wikipedia的论点有一篇简短的文章。