代码之家  ›  专栏  ›  技术社区  ›  Eli Bendersky

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

  •  107
  • Eli Bendersky  · 技术社区  · 17 年前

    这个问题实际上是一个 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 代码完成 :

    然后释放资源。用一个 你忘记的可能性 释放每个地方的资源

    这种方法的另一个支持来自 Linux设备驱动程序 this section .

    转到 在C?您是否更喜欢其他方法,这些方法会产生更复杂和/或效率更低的代码,但会避免 转到 ?

    16 回复  |  直到 10 年前
        1
  •  69
  •   Michael Burr    17 年前

    goto 转到 ,它正以定义良好、或多或少结构化的方式使用。

        2
  •  19
  •   Jonathan Leffler    17 年前

    你概述的是错误处理问题的通用解决方案——只要仔细使用,我就没问题。

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

        3
  •  17
  •   psmears Touffy    10 年前

    goto 用于获取清理代码。像任何编码技术一样,它也有优缺点,并不适合每种情况,但如果你选择一种风格,它值得考虑——特别是如果你想避免 转到 if s

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

    我给你看看 转到 版本优先,因为它更接近原始问题中的代码。

    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 另一个答案中描述的方法,如果初始化顺序发生变化,则 开关 必须非常仔细地编辑,以避免试图清理最初没有初始化的东西。

    prepare_stuff() malloc() open() ,则可以使用包含返回指针或文件描述符的变量,例如:

    int fd = -1;
    
    ....
    
    fd = open(...);
    if (fd == -1) {
        goto cleanup;
    }
    
    ...
    
    cleanup:
    
    if (fd != -1) {
        close(fd);
    }
    

    现在,如果我们用变量额外跟踪错误状态,我们可以避免 转到

    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 正在这里玩。如果你以一致的方式构建函数以返回错误,你甚至可以避免第二个错误 如果

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

    所以,这(仍然)是另一种可以用来解决这个问题的风格。正确使用它可以实现非常干净、一致的代码——就像任何技术一样,如果落入坏人之手,它最终可能会产生冗长而令人困惑的代码:-)

        4
  •  9
  •   dirkgently    17 年前

    问题在于 goto

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

    setjmp/longjmp

        5
  •  4
  •   webmarc    17 年前

    goto的速度非常快,特别是如果你小心地确保它能快速编译。非常适合速度要求很高的应用。对于其他应用程序,为了可维护性,使用if/else+情况来降低开销可能是有意义的。

    更新:以下是案例示例

    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 ; 
          } 
    } 
    
        6
  •  3
  •   toto    17 年前

    有时你想在函数中添加一些东西,只需一个goto就可以轻松完成。

        7
  •  1
  •   supercat    14 年前

    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;
    

    但如果清理只应该在函数失败时发生 转到 案件可以通过以下方式处理 return 返回 在标有的线上 ***** .

    在“即使在正常情况下也要清理”的情况下,我会考虑使用 转到 do while(0) 构建,除其他外,因为目标标签本身实际上比其他标签更大声地喊出“看着我” break 和 做 / 而(0) 返回 从可读性的角度来看,语句最终必须位于最糟糕的位置(返回语句通常应该位于函数的开头,或者位于“看起来”的末尾);拥有a 返回

    顺便说一句,我有时会使用 转到 因为错误处理在 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;
        }
      }   
    

    虽然一个可以取代 转到 声明与 {handle_error(); break;} ,虽然可以使用 做 / 而(0) 循环与 continue 转到 PACKET_ERROR 到处都是 goto PACKET_ERROR 虽然编译器可能会一次性写出重复的代码,并用跳转到该共享副本来替换大多数出现的代码,但使用 转到 使得更容易注意到设置数据包略有不同的地方(例如,如果“有条件执行”指令决定不执行)。

        8
  •  1
  •   Jonathan Leffler    14 年前

    我同意问题中给出的相反顺序的goto清理是大多数函数中最干净的清理方式。但我也想指出,有时候,你无论如何都希望你的功能能够清理干净。在这些情况下,我使用以下变体if 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
  •  1
  •   user1251840    10 年前

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

    我将在文中附上一小段,说明我认为关于goto的好主意。


    并且通常导致代码清晰度的提高。 非循环函数调用图,代码分析器可以利用它,并且可以 其中早期错误返回是更简单的解决方案。)


    禁止使用goto 但是:

    如果规则看起来 起初很严厉, 从字面上看,你的生活可能取决于它的正确性:用于控制的代码 你乘坐的飞机,离你住的地方几英里远的核电站,或者 将宇航员送入轨道的航天器。 这些规则就像你车里的安全带:

        10
  •  0
  •   Ryan Emerle    17 年前

    cleanup_3 应该进行清理,然后致电 cleanup_2 同样地, 应该进行清理,然后调用cleanup1。似乎你什么时候做 cleanup_[n] cleanup_[n-1] 因此,这应该是该方法的责任(例如, 清洁3 不打电话就永远打不过来

    这 goto 方法不是 错误的 或 不过,值得注意的是,这不一定是“最干净”的方法(依我之见)。

    如果你正在寻找最佳性能,那么我想 转到

        11
  •  0
  •   Simon Woodside    16 年前

    我认为,就给定的代码而言,这里的问题是错误的。

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

    自己清理

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

        12
  •  0
  •   Mikko Korkalo    10 年前

    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;
    }
    
        13
  •  0
  •   Alejandro Visiedo García    6 年前

    然而,旧的讨论。…那么使用“箭头反模式”并稍后将每个嵌套级别封装在静态内联函数中呢?代码看起来很干净,它是最佳的(当启用优化时),并且没有使用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;
    }
    

    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
    
        14
  •  0
  •   charles    5 年前

        15
  •  -1
  •   Nitin Kunal    14 年前

    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

        16
  •  -2
  •   TheCodeArtist    14 年前

    我们使用 Daynix CSteps 图书馆作为“ 转到问题 “在init函数中。
    看 here 和 here .