101

这个问题实际上是不久前在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;
}

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

goto 在分配资源、对这些资源执行操作、然后释放资源的例程中很有用。使用 goto,您可以清理代码的一部分。goto 减少了您忘记在检测到错误的每个地方释放资源的可能性。

对这种方法的另一种支持来自本节中的Linux 设备驱动程序一书。

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

4

16 回答 16

70

FWIF,我发现您在问题示例中给出的错误处理习语比迄今为止答案中给出的任何替代方法都更具可读性和更容易理解。虽然goto一般来说是一个坏主意,但当以简单统一的方式完成时,它对于错误处理很有用。在这种情况下,即使它是 a goto,它也以定义明确且或多或少结构化的方式使用。

于 2009-04-26T05:05:34.880 回答
20

作为一般规则,避免 goto 是一个好主意,但是当 Dijkstra 第一次写“GOTO 被认为是有害的”时普遍存在的滥用行为现在甚至没有成为大多数人的想法。

您概述的是错误处理问题的通用解决方案-只要仔细使用它,我就可以了。

您的特定示例可以简化如下(步骤 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;
}

继续这个过程:

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;
}

我相信,这相当于原始代码。这看起来特别干净,因为原始代码本身非常干净且组织良好。通常,代码片段没有那么整洁(尽管我接受他们应该如此的论点);例如,传递给初始化(设置)例程的状态通常比显示的多,因此也有更多的状态要传递给清理例程。

于 2009-04-25T14:13:03.887 回答
16

我很惊讶没有人提出这个替代方案,所以即使这个问题已经存在了一段时间,我还是会添加它:解决这个问题的一个好方法是使用变量来跟踪当前状态。goto这是一种无论是否用于获得清理代码都可以使用的技术。像任何编码技术一样,它有利也有弊,并且不会适用于所有情况,但如果您选择一种风格,则值得考虑 - 特别是如果您想避免goto最终没有深度嵌套if的 s。

基本思想是,对于每个可能需要采取的清理行动,都有一个变量,我们可以根据其值判断是否需要进行清理。

我将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;
}

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

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

int fd = -1;

....

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

...

cleanup:

if (fd != -1) {
    close(fd);
}

现在,如果我们另外使用变量跟踪错误状态,我们可以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;
}

同样,对此有潜在的批评:

  • 难道所有这些“如果”不会影响性能吗?否 - 因为在成功的情况下,无论如何您都必须进行所有检查(否则您不会检查所有错误情况);并且在失败的情况下,大多数编译器会将失败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);
    }
    

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

所以 - 这是(但)另一种可用于解决此问题的样式。正确使用它可以生成非常干净、一致的代码——就像任何技术一样,如果使用不当,它最终会产生冗长且令人困惑的代码:-)

于 2011-06-14T15:27:22.040 回答
9

关键字的问题goto大多被误解了。这不是普通的邪恶。您只需要注意使用每个 goto 创建的额外控制路径。很难推理您的代码及其有效性。

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

我们不使用 goto。更高的重要性放在返回值上。异常处理是通过setjmp/longjmp- 尽你所能。

于 2009-04-25T13:25:21.023 回答
4

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 ; 
      } 
} 
于 2009-04-25T13:32:41.770 回答
3

GOTO 很有用。这是您的处理器可以做的事情,这就是您应该有权访问它的原因。

有时你想在你的函数中添加一些东西,而单 goto 让你很容易做到这一点。可以节省时间。。

于 2009-04-26T01:09:20.940 回答
1

我个人是“编写安全关键代码的 10-10 条规则的力量”的追随者。

我将包含该文本中的一个小片段,说明我认为关于 goto 的一个好主意。


规则:将所有代码限制为非常简单的控制流结构——不要使用 goto 语句、setjmp 或 longjmp 结构以及直接或间接递归。

基本原理:更简单的控制流转化为更强大的验证能力,并且通常会提高代码的清晰度。递归的消除可能是这里最大的惊喜。但是,如果没有递归,我们可以保证有一个非循环的函数调用图,它可以被代码分析器利用,并且可以直接帮助证明所有应该有界的执行实际上都是有界的。(请注意,此规则并不要求所有函数都有一个返回点——尽管这通常也简化了控制流。不过,有足够多的情况,早期错误返回是更简单的解决方案。)


禁止使用 goto似乎很糟糕,但是:

如果这些规则一开始看起来很严厉,请记住,它们的目的是让检查代码成为可能,而实际上你的生活可能取决于其正确性:用于控制你所驾驶的飞机、核电站的代码距离您居住的地方或将宇航员送入轨道的航天器几英里。这些规则就像你车上的安全带:最初它们可能有点不舒服,但过了一段时间,它们的使用变成了第二天性,不使用它们变得难以想象。

于 2009-04-27T19:53:49.887 回答
1

一般来说,我认为可以最清楚地编写一段代码这一事实是程序流程可能比通常期望的更复杂goto症状。以奇怪的方式组合其他程序结构以避免使用goto将试图治疗症状,而不是疾病。如果没有,您的特定示例可能不会很难实现goto

  做 {
    .. 设置仅在提前退出的情况下才需要清理的东西1
    如果(错误)中断;
    做
    {
      ..设置在提前退出时需要清理的thing2
      如果(错误)中断;
      // ***** 见关于这一行的文字
    } 而(0);
    ..清理东西2;
  } 而(0);
  ..清理东西1;

但是如果清理只应该在函数失败时发生,则goto可以通过return在第一个目标标签之前放置 a 来处理这种情况。上面的代码需要return在标有*****.

在“即使在正常情况下也进行清理”的场景中,我认为使用 ofgotodo/构造更清晰,因为目标标签本身实际上比and /构造while(0)更能喊出“看我” 。对于“仅在错误时进行清理”的情况,从可读性的角度来看,语句最终必须处于最糟糕的位置(返回语句通常应该在函数的开头,或者在“看起来像”结束); 在目标标签之前有一个比在“循环”结束之前有一个更容易满足该条件。breakdowhile(0)returnreturn

顺便说一句,我有时goto用于错误处理的一种情况是在一个switch语句中,当多个案例的代码共享相同的错误代码时。尽管我的编译器通常足够聪明,可以识别出多个案例以相同的代码结尾,但我认为更清楚地说:

REPARSE_PACKET:
  开关(包[0])
  {
    案例 PKT_THIS_OPERATION:
      如果(问题条件)
        转到 PACKET_ERROR;
      ... 处理 THIS_OPERATION
      休息;
    案例 PKT_THAT_OPERATION:
      如果(问题条件)
        转到 PACKET_ERROR;
      ... 处理 THAT_OPERATION
      休息;
    ...
    案例 PKT_PROCESS_CONDITIONALLY
      如果 (packet_length < 9)
        转到 PACKET_ERROR;
      if (packet_condition 涉及数据包[4])
      {
        数据包长度-= 5;
        memmove(数据包,数据包+5,数据包长度);
        转到 REPARSE_PACKET;
      }
      别的
      {
        数据包[0] = PKT_CONDITION_SKIPPED;
        数据包[4] = 数据包长度;
        数据包长度 = 5;
        packet_status = READY_TO_SEND;
      }
      休息;
    ...
    默认:
    {
     数据包错误:
      packet_error_count++;
      数据包长度 = 4;
      数据包[0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      休息;
    }
  }   

虽然可以goto用. _ _ _ 此外,虽然可以从使用 的任何地方复制代码,并且编译器可能会写出重复的代码一次并用跳转到该共享副本来替换大多数出现的地方,但使用更容易注意到地方它对数据包的设置略有不同(例如,如果“有条件执行”指令决定不执行)。{handle_error(); break;}dowhile(0)continuegotoPACKET_ERRORgoto PACKET_ERRORgoto

于 2012-06-06T16:25:56.123 回答
1

我同意问题中给出的相反顺序的 goto 清理是在大多数功能中清理事物的最干净的方式。但我也想指出,有时候,无论如何你都希望你的函数清理干净。在这些情况下,我使用以下变体 if ( 0 ) { label: } 惯用语去正确的清理过程点:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }
于 2015-10-31T15:41:47.233 回答
0

在我看来,cleanup_3应该进行清理,然后调用cleanup_2. 同样,cleanup_2应该进行清理,然后调用 cleanup_1。似乎任何时候你都这样做cleanup_[n],这cleanup_[n-1]是必需的,因此它应该是方法的责任(例如,cleanup_3如果不调用就永远不能调用它cleanup_2,并且可能导致泄漏。)

鉴于这种方法,您只需调用清理例程,而不是 goto,然后返回。

不过,这种goto方法并没有不好,只是值得注意的是,它不一定是“最干净”的方法(恕我直言)。

如果您正在寻找最佳性能,那么我认为该goto解决方案是最好的。然而,我只希望它与少数性能关键的应用程序(例如,设备驱动程序、嵌入式设备等)相关。否则,这是一个优先级低于代码清晰度的微优化。

于 2009-04-25T13:49:09.157 回答
0

我认为这里的问题对于给定的代码是错误的。

考虑:

  1. do_something()、init_stuff() 和 prepare_stuff() 似乎知道它们是否失败,因为在这种情况下它们返回 false 或 nil。
  2. 设置状态的责任似乎是这些函数的责任,因为没有直接在 foo() 中设置状态。

因此:do_something()、init_stuff() 和 prepare_stuff() 应该自己进行清理。有一个单独的 cleanup_1() 函数在 do_something() 之后进行清理打破了封装的理念。这是糟糕的设计。

如果他们自己进行清理,那么 foo() 就变得非常简单。

另一方面。如果 foo() 实际上创建了自己的需要拆除的状态,那么 goto 将是合适的。

于 2010-05-29T07:50:45.720 回答
0

这是我的首选:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}
于 2016-09-30T08:51:21.543 回答
0

然而,旧的讨论......如何使用“箭头反模式”并稍后将每个嵌套级别封装在静态内联函数中?代码看起来很干净,它是最佳的(启用优化时),并且没有使用 goto。简而言之,分而治之。下面是一个例子:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

在空间方面,我们在堆栈中创建了三倍的变量,这并不好,但是在这个简单的例子中,当使用 -O2 从堆栈中删除变量并使用寄存器进行编译时,这会消失。我从上面的块中gcc -S -O2 test.c得到的是以下内容:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols
于 2020-01-19T10:50:40.983 回答
0

是的,它是 C 中异常的有效和最佳实践。任何语言的所有错误处理机制都只是从错误跳转到处理,如 goto 到标签。但请考虑将标签放在 goto 之后的执行流程中并且在同一范围内。

于 2021-03-25T17:07:12.783 回答
-1

我更喜欢使用以下示例中描述的技术...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

来源: http: //blog.staila.com/ ?p=114

于 2011-11-17T19:51:23.880 回答
-2

我们使用Daynix CSteps库作为init 函数中“ goto 问题”的另一种解决方案。
这里这里

于 2012-06-06T08:16:12.737 回答