有效使用goto进行C中的错误管理?

Eli*_*sky 88 c error-handling exception-handling goto

这个问题实际上是不久前在programming.reddit.com 上进行有趣讨论的结果.它基本归结为以下代码:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

goto这里的使用似乎是最好的方法,导致所有可能性中最干净,最有效的代码,或者至少在我看来.在Code Complete中引用Steve McConnell :

goto在分配资源,对这些资源执行操作,然后释放资源的例程中很有用.使用goto,您可以清理代码的一部分.goto可以降低忘记在检测到错误的每个位置释放资源的可能性.

此方法的另一个支持来自本节中的" Linux设备驱动程序"一书.

你怎么看?这种情况goto在C中是否有效?您是否更喜欢其他方法,这些方法会产生更复杂和/或效率更低的代码,但是要避免goto

Mic*_*urr 55

FWIF,我发现你在问题的例子中给出的错误处理习惯比目前为止答案中给出的任何替代方案更具可读性和易于理解.虽然goto通常是一个坏主意,但是当以简单和统一的方式完成时,它对于错误处理是有用的.在这种情况下,即使它是a goto,它也是以明确定义的,或多或少结构化的方式使用的.

  • @StartupCrazy,我知道这已经有好几年了,但是为了这个网站上帖子的有效性,我会指出他不能.如果他在代码中遇到错误goto error3,他会清理1 2和3,在你的sugeested解决方案中他只会运行清理3.他可以嵌套东西,但这只是箭头反模式,程序员应该避免. (7认同)

Jon*_*ler 16

作为一般规则,避免使用goto是一个好主意,但是当Dijkstra第一次写"GOTO Considered Harmful"时流行的滥用行为现在甚至没有超过大多数人的想法.

您概述的是错误处理问题的一般解决方案 - 只要仔细使用它就可以了.

您的特定示例可以简化如下(步骤1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

继续这个过程:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

我认为,这相当于原始代码.这看起来特别干净,因为原始代码本身非常干净且组织良好.通常,代码片段并不像那样整洁(虽然我接受他们应该的论点); 例如,经常有更多的状态传递给初始化(设置)例程而不是显示,因此更多的状态也传递给清理例程.

  • 是的,嵌套解决方案是可行的替代方案之一.不幸的是,随着嵌套水平的加深,它变得不那么可行. (22认同)
  • 不幸的是,这不适用于循环 - 如果在循环内发生错误,那么goto比设置和检查标志和'break'语句(它们只是巧妙地伪装的getos)的替代方法要清晰得多. (6认同)
  • 我知道我在这里不知情,但我发现这个建议相当差 - 应该避免[箭头反模式](http://c2.com/cgi/wiki?ArrowAntiPattern). (5认同)
  • @eliben:同意 - 但更深层次的嵌套可能(可能是)表明您需要引入更多功能,或者准备步骤更多,或者重构您的代码.我可以争辩说,每个准备功能都应该进行设置,调用链中的下一个,并进行自己的清理.它本地化了这项工作 - 你甚至可以节省三个清理功能.它还部分取决于是否在任何其他调用序列中使用(可用)任何设置或清除功能. (4认同)
  • @Smith,更像是不穿防弹背心开车。 (2认同)

psm*_*ars 15

我很惊讶没有人提出这个替代方案,所以尽管这个问题已经存在了一段时间我会加入它:解决这个问题的一个好方法是使用变量来跟踪当前状态.这是一种可以使用的技术,无论是否goto用于到达清理代码.像任何编码技术一样,它有优点和缺点,并不适合所有情况,但如果你选择一种风格,值得考虑 - 特别是如果你想避免goto不用深度嵌套的ifs.

基本思想是,对于可能需要采取的每个清理操作,都有一个变量,从中可以判断清理是否需要进行清理.

我将goto首先显示该版本,因为它更接近原始问题中的代码.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

与其他一些技术相比,这样做的一个优点是,如果初始化函数的顺序发生变化,仍然会发生正确的清理 - 例如,使用switch另一个答案中描述的方法,如果初始化顺序发生变化,那么switch必须非常仔细地编辑,以避免尝试清理一些事实上没有初始化的东西.

现在,有些人可能认为这种方法增加了许多额外的变量 - 事实上在这种情况下确实如此 - 但实际上现有的变量通常已经跟踪或者可以跟踪所需的状态.例如,如果prepare_stuff()实际上是对malloc()或的调用open(),则可以使用保存返回的指针或文件描述符的变量 - 例如:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我们另外使用变量跟踪错误状态,我们可以goto完全避免,并且仍然可以正确清理,而不会让缩进越来越深,我们需要的初始化越多:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

同样,有可能批评这一点:

  • 不是所有那些"如果"伤害表现?不 - 因为在成功的情况下,你必须做所有的检查(否则你没有检查所有的错误情况); 在失败的情况下,大多数编译器会将失败if (oksofar)检查的顺序优化为单个跳转到清理代码(GCC肯定会) - 并且在任何情况下,错误情况通常对性能不太重要.
  • 这不是另外一个变量吗?在这种情况下是,但通常该return_value变量可用于oksofar扮演在这里播放的角色.如果您构建函数以一致的方式返回错误,您甚至可以避免if在每种情况下的第二个:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这样编码的一个优点是,一致性意味着原始程序员忘记检查返回值的任何地方都像拇指一样伸出,这样就更容易找到(那一类)错误.

所以 - 这是(还)一种可以用来解决这个问题的风格.正确使用它可以提供非常干净,一致的代码 - 就像任何技术一样,在错误的手中它最终会产生冗长且令人困惑的代码:-)

  • 看起来你迟到了,但我当然喜欢这个答案! (2认同)

dir*_*tly 8

goto关键字的问题大多被误解了.这不是简单的邪恶.您只需要了解每个goto创建的额外控制路径.很难推断出你的代码及其有效性.

FWIW,如果你查看developer.apple.com教程,他们会采用goto方法来处理错误.

我们不使用gotos.对返回值的重要性更高.异常处理是通过setjmp/longjmp- 无论你能做什么.

  • 虽然我确实在某些需要它的情况下使用了setjmp/longjmp,但我认为它甚至比goto"更糟糕"(我也使用它,当它被调用时稍微不那么保守).我使用setjmp/longjmp的唯一时间是(1)目标将以不受其当前状态影响的方式关闭所有内容,或者(2)目标将重新初始化内部控制的所有内容setjmp/longjmp保护块的方式独立于其当前状态. (8认同)

web*_*arc 5

goto 语句在道德上没有任何错误,就像 (void)* 指针在道德上没有错误一样。

一切都取决于您如何使用该工具。在您提出的(简单的)案例中,案例语句可以实现相同的逻辑,尽管开销更大。真正的问题是“我的速度要求是多少?”

goto 的速度非常快,尤其是当您小心确保它编译为短跳转时。非常适合注重速度的应用。对于其他应用程序,为了可维护性,使用 if/else + case 来承受开销可能是有意义的。

请记住:goto 不会杀死应用程序,而是开发人员杀死应用程序。

更新:这是案例示例

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 
Run Code Online (Sandbox Code Playgroud)

  • @webmarc,抱歉,但这太可怕了!您刚刚完全模拟了带有标签的 goto - 为标签发明了自己的非描述性值并使用 switch/case 实现了 goto。Failure_value = 1 比“goto cleanup_something”更干净? (6认同)
  • 我觉得你把我安排在这里......你的问题是为了征求意见,以及我更喜欢什么。然而,当我给出答案时,这太可怕了。:-( 至于您对标签名称的抱怨,它们与示例的其余部分一样具有描述性:cleanup_1、foo、bar。当标签名称与问题无关时,您为什么要攻击标签名称? (5认同)