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

时间:2009-04-25 13:17:10

标签: c exception-handling error-handling goto

此问题实际上是不久前在programming.reddit.com上interesting discussion的结果。它基本上归结为以下代码:

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的使用似乎是最好的方法,导致所有可能性中最干净,最有效的代码,或者至少在我看来。在代码完成中引用Steve McConnell:

  

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

此方法的另一个支持来自this section中的 Linux设备驱动程序一书。

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

16 个答案:

答案 0 :(得分:53)

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

答案 1 :(得分: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;
}

继续这个过程:

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

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

答案 2 :(得分:15)

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

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

我会首先显示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);
    }
    

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

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

答案 3 :(得分:8)

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

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

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

答案 4 :(得分: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 ; 
      } 
} 

答案 5 :(得分:2)

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

有时你想为你的功能添加一些东西,而单一的转到让你轻松地做到这一点。 它可以节省时间..

答案 6 :(得分:1)

我个人是"The Power of Ten - 10 Rules for Writing Safety Critical Code"

的追随者

我将在该文本中添加一个小片段,说明我认为对goto的好主意。


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

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


放弃使用转到似乎不好,但是:

如果规则如此 一开始是严酷的,请记住,它们是为了能够检查代码 从字面上看,你的生活可能取决于它的正确性:用于控制的代码 你乘坐的飞机,离你住的地方几英里的核电站,或者 将宇航员送入轨道的航天器。 这些规则就像你车里的安全带: 最初他们可能有点不舒服,但过了一段时间他们的使用就变成了 第二种性质而不使用它们变得难以想象。

答案 7 :(得分:1)

一般来说,我认为使用goto作为症状可以最清楚地编写一段代码,以致程序流程可能比通常所希望的更复杂。以奇怪的方式结合其他程序结构以避免使用goto将试图治疗症状,而不是疾病。如果没有goto

,您的特定示例可能不会太难实现
  do {
    .. set up thing1 that will need cleanup only in case of early exit
    if (error) break;
    do
    {
      .. set up thing2 that will need cleanup in case of early exit
      if (error) break;
      // ***** SEE TEXT REGARDING THIS LINE
    } while(0);
    .. cleanup thing2;
  } while(0);
  .. cleanup thing1;

但如果仅在函数失败时才进行清理,则可以通过在第一个目标标签之前放置goto来处理return案例。上述代码需要在标有return的行中添加*****

在“正常情况下的清理”场景中,我认为使用gotodo / while(0)构造更清晰,因为目标标签他们自己实际上比breakdo / while(0)结构更多地“向我看”。对于“仅在出错时清理”的情况,return语句最终必须处于可读性的最差位置(返回语句通常应该在函数的开头,或者在什么“看起来像”结束);在目标标签之前有一个return比在一个“循环”结束之前更容易满足该标准。

BTW,当有多个案例的代码共享相同的错误代码时,有时使用goto进行错误处理的一种情况是switch语句。尽管我的编译器通常足够聪明,能够识别多个案例以相同的代码结束,但我认为更清楚地说:

 REPARSE_PACKET:
  switch(packet[0])
  {
    case PKT_THIS_OPERATION:
      if (problem condition)
        goto PACKET_ERROR;
      ... handle THIS_OPERATION
      break;
    case PKT_THAT_OPERATION:
      if (problem condition)
        goto PACKET_ERROR;
      ... handle THAT_OPERATION
      break;
    ...
    case PKT_PROCESS_CONDITIONALLY
      if (packet_length < 9)
        goto PACKET_ERROR;
      if (packet_condition involving packet[4])
      {
        packet_length -= 5;
        memmove(packet, packet+5, packet_length);
        goto REPARSE_PACKET;
      }
      else
      {
        packet[0] = PKT_CONDITION_SKIPPED;
        packet[4] = packet_length;
        packet_length = 5;
        packet_status = READY_TO_SEND;
      }
      break;
    ...
    default:
    {
     PACKET_ERROR:
      packet_error_count++;
      packet_length = 4;
      packet[0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      break;
    }
  }   

虽然可以用goto替换{handle_error(); break;}语句,虽然可以使用do / while(0)循环和continue来处理包裹条件执行包,我真的不认为比使用goto更清楚。此外,尽管可以在PACKET_ERROR处复制出goto PACKET_ERROR的代码,而且编译器可能会写出重复的代码一次,并通过跳转到该共享副本来替换大多数事件,使用goto可以更容易地注意到设置数据包的位置有点不同(例如,如果“有条件执行”指令决定不执行)。

答案 8 :(得分: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 ;
 }

答案 9 :(得分:0)

我觉得cleanup_3应该进行清理,然后拨打cleanup_2。同样,cleanup_2应该进行清理,然后调用cleanup_1。看来,只要您执行cleanup_[n]cleanup_[n-1]就是必需的,因此它应该是该方法的责任(例如,如果不调用{{cleanup_3,就永远无法调用cleanup_2 1}}并可能导致泄漏。)

考虑到这种方法,你只需要调用清理例程,然后返回。

goto方法不是错误错误,但值得注意的是,它不一定是“最干净”的方法(恕我直言)。

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

答案 10 :(得分:0)

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

考虑:

  1. do_something(),init_stuff()和prepare_stuff()似乎知道它们是否失败,因为在这种情况下它们返回false或nil。
  2. 设置状态的责任似乎是这些功能的责任,因为没有直接在foo()中设置状态。
  3. 因此:do_something(),init_stuff()和prepare_stuff()应进行自己的清理。在do_something()之后清理单独的cleanup_1()函数会破坏封装的原理。这是糟糕的设计。

    如果他们做了自己的清理,那么foo()变得非常简单。

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

答案 11 :(得分: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;
}

答案 12 :(得分: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

答案 13 :(得分:0)

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

答案 14 :(得分:-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

答案 15 :(得分:-1)

我们使用Daynix CSteps库作为init函数中“转到问题”的另一种解决方案。
请参阅herehere