C或C++中良好的goto示例

79
在这个主题中,我们将查看C或C ++中goto的良好使用示例。这受到答案的启发,因为人们认为我在开玩笑而被投票提升。
摘要(标签已更改以使意图更清晰):
infinite_loop:

    // code goes here

goto infinite_loop;

为什么它比其他替代方案更好:

  • 它很具体。 goto是语言结构,会导致无条件分支。而其他替代方案则依赖于使用支持有条件分支的结构,并带有一个退化了的总是为真的条件。
  • 标签记录了意图,不需要额外的注释。
  • 读者不必扫描中间代码以寻找早期的break(尽管不道德的黑客仍然可以通过早期的goto模拟continue)。

规则:

  • 假设gotophobes没有获胜。我们知道上述内容不能在实际代码中使用,因为它违反了已建立的习惯用法。
  • 假设我们都听说过'Goto considered harmful',并知道goto可用于编写Dijkstra式的spaghetti code。
  • 如果您不同意某个示例,请仅基于技术优势进行批评(“因为人们不喜欢goto”不是技术原因)。

让我们看看能否像成年人一样谈论这个问题。

编辑

这个问题现在看起来已经完成了。它产生了一些高质量的答案。感谢每个人,特别是那些认真考虑了我的小循环示例的人。大多数怀疑论者都担心缺乏块作用域。正如@quinmars在评论中指出的那样,您总是可以在循环体周围放置大括号。我顺便提一下,for(;;)while(true)也没有免费提供大括号(省略它们可能会引起恼人的错误)。无论如何,我不会再浪费你的脑力在这个琐碎的问题上 - 我可以接受无害且惯用的for(;;)while(true)(如果我想保住自己的工作的话)。

考虑到其他回答,我发现许多人认为goto始终需要以另一种方式重写。当然,您可以通过引入循环、一个额外的标志、嵌套的if语句堆栈或任何其他方法来避免goto,但是为什么不考虑是否goto是执行该任务的最佳工具呢?换句话说,人们愿意忍受多少丑陋的代码才能避免使用用于其预期目的的内置语言特性?我的看法是甚至添加一个标志也是太高的代价。我喜欢我的变量表示问题或解决方案领域中的事物。‘仅仅为了避免goto’无法达到这个目的。

我会接受第一个给出分支到清理块的C语言模式的答案。在我看来,这是所有发布的答案中对于使用goto最强有力的论据,特别是如果你通过避免它所需要做的扭曲操作来衡量的话。


11
我不明白为什么反对使用goto的人不直接在项目的包含文件顶部强制添加“#define goto report_to_your_supervisor_for_re_education_through_labour”,如果它总是错误的,那就让其无法实现。否则,有时候它是正确的... - Steve Jessop
14
“无法在真实代码中使用”?每当它是完成工作的最佳工具时,我都会在“真实”的代码中使用它。 “成熟的习惯用语”是“盲目教条主义”的委婉说法。 - Dan Moulding
7
我同意“goto”可以很有用(以下有很好的例子),但是我不同意你的具体例子。一行代码写着“goto infinite_loop”听起来像是要“去到代码中我们将永远循环的那部分”,就像“initialize(); set_things_up(); goto infinite_loop;”一样,而实际上你想传达的是“开始我们已经在的循环的下一个迭代”,这是完全不同的。如果你想循环,并且你的语言具有专门为循环设计的结构,请使用它们以获得更清晰的表述。while(true){foo()}非常明确无误。 - Josh
6
你所提供的例子存在一个很大的缺点,就是不清楚代码中哪些其他位置可能会跳转到该标签。而一个“正常”的无限循环 (for (;;)) 没有令人惊讶的入口点。 - jalf
3
@György:是的,但那不是一个入口点。 - jalf
显示剩余3条评论
16个回答

87

这里是一个我听说过的人们使用的技巧。虽然我从未在实际中看到过它。而且它仅适用于C,因为C++具有更符合惯用法的RAII来执行此操作。

void foo()
{
    if (!doA())
        goto exit;
    if (!doB())
        goto cleanupA;
    if (!doC())
        goto cleanupB;

    /* everything has succeeded */
    return;

cleanupB:
    undoB();
cleanupA:
    undoA();
exit:
    return;
}

2
除了注释不理解换行符之外,这段代码有什么问题吗?void foo() { if (doA()) { if (doB()) { if (doC()) { /* 所有操作都成功 */ return; } undoB(); } undoA(); } return; } - Artelius
6
多余的代码块会导致不必要的缩进,使得代码难以阅读。如果其中一个条件在循环内部,则代码也无法正常工作。 - Adam Rosenfield
26
在大多数低级Unix东西中(例如Linux内核),你可以看到很多这种代码。在C语言中,我认为这是最好的错误恢复习惯用法。 - David Cournapeau
8
正如cournape所提到的,Linux内核一直都在使用这种风格。 - CesarB
8
不仅Linux内核采用这种方式,从微软的Windows驱动程序示例中也可以找到同样的模式。总的来说,这是一种处理异常的C语言方式,非常有用:)。我通常只喜欢使用1个标签,在极少数情况下使用2个。在99%的情况下可以避免使用3个标签。 - Ilya
显示剩余6条评论

85

C语言中使用GOTO的典型需求如下:

for ...
  for ...
    if(breakout_condition) 
      goto final;

final:

没有简单的方法可以在没有goto语句的情况下跳出嵌套循环。


49
通常情况下,当出现这种需求时,我可以使用返回(return)代替。但是您需要编写小函数才能使其按照这种方式工作。 - Darius Bacon
7
我完全同意Darius的观点 - 将其重构为一个函数并返回它! - metao
9
@metao:Bjarne Stroustrup不同意。在他的C++编程语言书中,这正是一个“良好使用”goto的示例。 - paercebal
19
使用goto语句的“根本问题”,正如Dijkstra所强调的那样,是结构化编程提供了更易理解的结构。为了避免使用goto而使代码变得更难阅读,这证明作者未能理解该篇文章的内容。 - Steve Jessop
5
不仅有人误解了这篇文章,大多数“不去”的狂热分子也不理解它写作时的历史背景。 - JUST MY correct OPINION
显示剩余8条评论

32

这里是一个不太幼稚的例子(来自Stevens的《UNIX环境高级编程》),介绍了Unix系统调用可能会被信号中断的情况。

restart:
    if (system_call() == -1) {
        if (errno == EINTR) goto restart;

        // handle real errors
    }

另一种选择是退化循环。这个版本的读法就像是英语“如果系统调用被信号中断了,重新启动它”。


3
这种方法例如Linux调度器中就使用了类似的跳转方式,但我认为只有非常少数情况下才能接受后退跳转,一般应该避免使用。 - Ilya
8
@Amarghosh,“continue”只是一种掩盖了的“goto”。 - jball
2
@jball ...嗯..就论证而言,是的;你可以用goto写出良好易读的代码,也可以用continue写出混乱不堪的代码..最终取决于编写代码的人。 重点是使用goto比使用continue更容易迷失方向。 新手通常会为每个问题使用他们得到的第一支“锤子”。 - Amarghosh
2
@Amarghosh。你的“改进”代码甚至不能等同于原始代码——在成功情况下它会无限循环。 - fizzer
3
@fizzer 哎呀,这需要两个else break才能达到等效的效果(每个if语句各一个)...我能说什么呢...无论你用不用goto,你的代码都只有你自己那么好。 - Amarghosh
显示剩余4条评论

14

如果Duff's设备不需要使用goto,那么你也不应该使用!;)

void dsend(int count) {
    int n;
    if (!count) return;
    n = (count + 7) / 8;
    switch (count % 8) {
      case 0: do { puts("case 0");
      case 7:      puts("case 7");
      case 6:      puts("case 6");
      case 5:      puts("case 5");
      case 4:      puts("case 4");
      case 3:      puts("case 3");
      case 2:      puts("case 2");
      case 1:      puts("case 1");
                 } while (--n > 0);
    }
}

以上代码来源于维基百科条目


2
来吧,伙计们,如果你要点踩,简短的评论说明原因会很好。 :) - Mitch Wheat
10
这是一种逻辑谬误的例子,也被称为“权威引证”。 在维基百科上查看。 - Marcus Griep
不过,不要在你的代码中使用这个设备——在现代机器上,它通常比memcpy()慢。 - Billy ONeal
1
Duff's Device是证明为什么goto不会有害的证据:它不使用goto,但仍然像人类很少见到的意大利面条一样。如果这是防止使用goto的唯一方法,我更喜欢后者。因此,提到Duff's Device加0.5分,但将其用于反对goto则扣1.1分。 - glglgl
@glglgl:switch语句是C语言版本的HP Basic语言中的GOTO I OF xx,yy,zz语句。目标由单词case引导,且具有作用域限制,但switch结构非常类似于基于goto的结构,这可以从其穿透规则得到证明。 - supercat

14

Knuth写了一篇名为“使用GOTO语句的结构化编程”的论文,您可以从这里获取。您将在那里找到许多示例。


2
那篇论文过时得不可开交,简直笑死人了。Knuth 在那里做的假设现在已经不成立了。 - Konrad Rudolph
3
哪些假设?他举的例子用伪代码描述,对于C语言这样的过程性编程语言而言,今天仍然是实际存在的,就像当时一样。 - zvrba

12

非常常见。

do_stuff(thingy) {
    lock(thingy);

    foo;
    if (foo failed) {
        status = -EFOO;
        goto OUT;
    }

    bar;
    if (bar failed) {
        status = -EBAR;
        goto OUT;
    }

    do_stuff_to(thingy);

OUT:
    unlock(thingy);
    return status;
}

我只在需要向前跳出块时使用goto,而且从不使用它来进入块中。这样可以避免滥用do{}while(0)等构造,同时仍保持可读性和结构良好的代码。


5
我认为这是典型的C语言错误处理方式。我看不到有更好、更易读的替代方法。谢谢。 - Friedrich
为什么不这样做呢...do_stuff(thingy) { RAIILockerObject(thingy); ..... /* -->*/ } /* <---*/ :) - Петър Петров
1
@ПетърПетров 这是C++中没有在C语言中的模式。 - ephemient
甚至比C++还要结构化的语言中,可以使用try { lock();..code.. } finally { unlock(); },但是 C 没有相应的语法。 - Blindy

12

我并不反对使用goto语句,但是我可以想到好几个原因,说明为什么你不应该在像你提到的循环中使用它:

  • 它没有限制作用域,因此你在其中使用的任何临时变量直到以后才会被释放。
  • 它没有限制作用域,因此可能会导致错误。
  • 它没有限制作用域,因此您无法在同一作用域内将来的代码中重复使用相同的变量名称。
  • 它没有限制作用域,因此可能会遗漏掉变量声明。
  • 人们不习惯使用它,这将使您的代码更难阅读。
  • 这种类型的嵌套循环可能导致代码混乱,普通的循环不会导致代码混乱。

1
是inf_loop: {/* 循环体 */} 跳转到inf_loop; 更好吗? :) - quinmars
2
  1. 即使没有大括号,这个例子中的第1点也是有问题的,因为它是一个无限循环。
  2. 太模糊了,无法解决。
  3. 试图隐藏一个与封闭作用域中同名的变量是不好的做法。
  4. 向后跳转不能跳过声明。
- fizzer
1-4 对于所有无限循环,通常在中间有某种跳出条件。因此它们是适用的。关于向后的goto不能跳过声明...并非所有的goto都是向后的... - Brian R. Bondy

7

@fizzer.myopenid.com: 您发布的代码片段等同于以下内容:

    while (system_call() == -1)
    {
        if (errno != EINTR)
        {
            // handle real errors

            break;
        }
    }

我更喜欢这种表单。

1
好的,我知道break语句只是goto语句的一种更可接受的形式。 - Mitch Wheat
10
在我看来,这很令人困惑。这是一个在正常执行路径中不期望进入的循环,所以逻辑似乎是相反的。虽然这是个观点问题。 - fizzer
1
倒过来?它开始,它继续,它穿过了。我认为这种形式更明显地表达了它的意图。 - Mitch Wheat
8
我同意fizzer的观点,goto语句提供了一个更清晰的条件值预期。 - Marcus Griep
4
这段代码相对于fizzer的代码,以何种方式更易于阅读? - erikkallen
显示剩余4条评论

7

在一个可以在多个点中止且每个点都需要不同级别清理的过程中,goto语句是一个好的使用场所。虽然gotophobes总是可以用结构化代码和一系列测试来替换goto,但我认为这更直接,因为它消除了过度缩进:

如果无法打开数据文件,则转到quit;
如果无法从文件获取数据,则转到closeFileAndQuit;
如果无法分配一些资源,则转到freeResourcesAndQuit;
// 在此处执行更多工作....
freeResourcesAndQuit: // 释放资源 closeFileAndQuit: // 关闭文件 quit: // 退出!

我会用一组函数替换它:freeResourcesCloseFileQuitcloseFileQuitquit - RCIX
4
如果你创建了这3个额外的函数,你需要传递指向需要释放/关闭的资源和文件的指针/句柄。你可能会让freeResourcesCloseFileQuit()调用closeFileQuit(),而closeFileQuit()将调用quit()。现在你有4个紧密耦合的函数要维护,其中3个最多只会被单个函数调用。如果你坚持避免使用goto,我认为嵌套的if()块开销更小,更容易阅读和维护。你通过这3个额外的函数获得了什么好处? - Adam Liss

6

尽管我随着时间的推移开始讨厌这种模式,但它已经深深地植入到了COM编程中。

#define IfFailGo(x) {hr = (x); if (FAILED(hr)) goto Error}
...
HRESULT SomeMethod(IFoo* pFoo) {
  HRESULT hr = S_OK;
  IfFailGo( pFoo->PerformAction() );
  IfFailGo( pFoo->SomeOtherAction() );
Error:
  return hr;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接