【问题标题】:Is there a better way to do C style error handling?有没有更好的方法来进行 C 风格的错误处理?
【发布时间】:2011-08-06 08:21:18
【问题描述】:

我正在尝试通过编写一个简单的解析器/编译器来学习 C。到目前为止,这是一次非常有启发性的体验,但是由于我拥有 C# 的深厚背景,我在调整时遇到了一些问题 - 特别是在缺乏异常方面。

现在我已经阅读了Cleaner, more elegant, and harder to recognize 并且我同意那篇文章中的每一个字;在我的 C# 代码中,我尽可能避免抛出异常,但是现在我面临一个我无法抛出异常的世界,我的错误处理完全淹没了原本干净且易于阅读的内容我的代码逻辑。

目前,我正在编写代码,如果出现问题,它需要快速失败,而且它也可能深度嵌套 - 我已经确定了一种错误处理模式,即“Get”函数出错时返回 NULL,其他函数在失败时返回 -1。在这两种情况下,失败的函数都会调用NS_SetError(),因此调用函数需要做的就是清理并在失败时立即返回。

我的问题是我所拥有的if (Action() < 0) return -1; 语句的数量正在引起我的注意 - 它非常重复并且完全掩盖了底层逻辑。我最终为自己创建了一个简单的宏来尝试改善这种情况,例如:

#define NOT_ERROR(X)    if ((X) < 0) return -1

int NS_Expression(void)
{
    NOT_ERROR(NS_Term());
    NOT_ERROR(Emit("MOVE D0, D1\n"));

    if (strcmp(current->str, "+") == 0)
    {
        NOT_ERROR(NS_Add());
    }
    else if (strcmp(current->str, "-") == 0)
    {
        NOT_ERROR(NS_Subtract());
    }
    else
    {
        NS_SetError("Expected: operator");
        return -1;
    }
    return 0;
}

NS_TermNS_AddNS_Subtract 中的每一个函数都会执行 NS_SetError() 并在出错的情况下返回 -1 - 它的 更好,但它仍然感觉像我在滥用宏并且不允许进行任何清理(某些函数,特别是返回指针的 Get 函数更复杂,需要运行清理代码)。

总的来说,感觉就像我错过了一些东西 - 尽管以这种方式处理错误据说更容易识别,但在我的许多函数中,我真的很难确定错误是否得到正确处理:

  • 某些函数在出错时返回 NULL
  • 某些函数在出错时返回 &lt; 0
  • 某些函数永远不会产生错误
  • 我的函数执行NS_SetError(),但许多其他函数没有。

有没有更好的方法来构建我的函数,或者其他人也有这个问题?

还有 Get 函数(返回指向对象的指针)在错误时返回 NULL 是个好主意,还是只是混淆了我的错误处理?

【问题讨论】:

  • 我曾经认为在失败时返回 NULL 是个好主意,直​​到最近我处理一些继承代码时,函数可能由于各种原因返回 NULL。调试空指针并不好玩:)
  • 滥用宏?你从未见过真正的宏滥用,儿子。
  • @zneak 我看到的宏导致了对宏的恐惧,因此我非常希望避免使用它们! :-)
  • @Skurmedel 引导我返回NULL 的原因是 SDL。这似乎是一个简洁的模式,因为这意味着您不需要单独测试 NULL 的 out 指针。

标签: c error-handling


【解决方案1】:

当您必须在每个 return 之前重复相同的最终代码时,这是一个更大的问题。在这种情况下,使用goto 被广泛接受:

int func ()
{
  if (a() < 0) {
    goto failure_a;
  }

  if (b() < 0) {
    goto failure_b;
  }

  if (c() < 0) {
    goto failure_c;
  }

  return SUCCESS;

  failure_c:
  undo_b();

  failure_b:
  undo_a();

  failure_a:
  return FAILURE;
}

你甚至可以围绕它创建自己的宏来节省你的打字时间,就像这样(虽然我还没有测试过):

#define CALL(funcname, ...) \
  if (funcname(__VA_ARGS__) < 0) { \ 
    goto failure_ ## funcname; \
  }

总的来说,它是一种比琐碎处理更清洁且更少冗余的方法:

int func ()
{
  if (a() < 0) {
    return FAILURE;
  }

  if (b() < 0) {
    undo_a();
    return FAILURE;
  }

  if (c() < 0) {
    undo_b();
    undo_a();
    return FAILURE;
  }

  return SUCCESS;
}

作为附加提示,我经常使用链接来减少代码中if 的数量:

if (a() < 0 || b() < 0 || c() < 0) {
  return FAILURE;
}

由于|| 是一个短路运算符,上面将替换三个独立的if。考虑在return 语句中使用链接:

return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;

【讨论】:

    【解决方案2】:

    一种清理技术是使用一个永远不会真正迭代的 while 循环。它为您提供 goto 而无需使用 goto。

    #define NOT_ERROR(x) if ((x) < 0) break;
    #define NOT_NULL(x) if ((x) == NULL) break;
    
    // Initialise things that may need to be cleaned up here.
    char* somePtr = NULL;
    
    do
    {
        NOT_NULL(somePtr = malloc(1024));
        NOT_ERROR(something(somePtr));
        NOT_ERROR(somethingElse(somePtr));
        // etc
    
        // if you get here everything's ok.
        return somePtr;
    }
    while (0);
    
    // Something went wrong so clean-up.
    free(somePtr);
    return NULL;
    

    但你会失去一定程度的缩进。

    编辑:我想补充一点,我并不反对 goto,只是对于提问者的用例,他并不真正需要它。在某些情况下,使用 goto 可以击败任何其他方法,但这不是其中之一。

    【讨论】:

    • 你能解释得更深入一点吗?
    • 使用 while 循环的“中断”功能可以让您直接进入需要清理的地方。谁投了反对票,请解释为什么?
    • @Kragen 用另一种方式说:“类似于 10,但表示为 1+2+3+4”(“只是没有 10”)。 +1,我也喜欢这个想法,但我不喜欢你认为它更好“因为它没有 goto”。
    • 如果您有多种可能的错误情况,其中一些共享清理而一些不共享,该怎么办?这将在简单的情况下工作,但即便如此,对于相当简单(并且可以说更清晰)的 goto 来说,这似乎是一个相当迟钝的解决方法。
    • 如果您需要在循环或另一个 while 内进行错误检查,此解决方案将失败。宏的 break 将跳出循环/while,但不会跳出外部 while (0)
    【解决方案3】:

    您可能不喜欢听到这个,但 C 语言处理异常的方法是 via the goto statement。这是它在语言中的原因之一。

    另一个原因是goto是状态机实现的自然表达。状态机最能代表什么常见的编程任务?一个词法分析器。有时查看lex 的输出。转到。

    所以在我看来,现在是时候让您熟悉语言语法元素中的那种贱民了,goto

    【讨论】:

    • 有一些方法可以在不显式使用 goto 的情况下做到这一点。
    • @James - 是的,有。然而,随着事情变得复杂,这些方式变得越来越尴尬。在某些时候,你最好承认你的控制流是非结构化的,而不是试图进一步掩盖它,这样你就可以假装它不是。
    • 我们必须同意不同意!它仅使用 try {} catch 块来模糊控制流。
    • @James - 检查标签。这是一道C题。 try/catch 不可用。
    • 我以为你会这么说,但程序流程完全一样,并且 try/catch 有效。
    【解决方案4】:

    除了goto,标准C 有另一个结构来处理异常流控制setjmp/longjmp。它的优点是您可以比某人提出的break 更容易地打破多重嵌套控制语句,并且除了goto 提供的内容之外,还有一个状态指示,可以编码出错的原因。

    另一个问题只是你的构造的语法。使用可能无意添加的控制语句不是一个好主意。你的情况

    if (bla) NOT_ERROR(X);
    else printf("wow!\n");
    

    会从根本上出错。我会使用类似的东西

    #define NOT_ERROR(X)          \
      if ((X) >= 0) { (void)0; }  \
      else return -1
    

    改为。

    【讨论】:

      【解决方案5】:

      这必须至少从两个层面考虑:您的函数如何交互,以及当它中断时您会做什么。

      我看到的大多数大型 C 框架总是通过引用返回状态和“返回”值(WinAPI 和许多 C Mac OS API 就是这种情况)。你想返回一个布尔值吗?

      StatusCode FooBar(int a, int b, int c, bool* output);
      

      你想返回一个指针吗?

      StatusCode FooBar(int a, int b, int c, char** output);
      

      嗯,你明白了。

      在调用函数方面,我最常看到的模式是使用指向清理标签的 goto 语句:

          if (statusCode < 0) goto error;
      
          /* snip */
          return everythingWentWell;
      
      error:
          cleanupResources();
          return somethingWentWrong;
      

      【讨论】:

        【解决方案6】:

        这个怎么样?

        int NS_Expression(void)
        {
            int ok = 1;
            ok = ok && NS_Term();
            ok = ok && Emit("MOVE D0, D1\n");
            ok = ok && NS_AddSub();
            return ok
        }
        

        【讨论】:

        • 这意味着如果NS_Term 失败,我们仍然会继续调用EmitNS_AddSub,这可能会导致坏事发生。
        • 你确定?我的程序都不起作用...在该代码中,如果一切正常,我假设这些函数返回 1...奇怪的是 NS_Expression 的返回值,应该返回 ok,我会编辑它。
        • 哦,等等...是的,我现在看到了 - 抱歉,来晚了!
        • 聪明,但如果你有其他语句之间呢?你不能把这个写成return NS_Term() &amp;&amp; Emit("MOVE D0, D1\n") &amp;&amp; NS_AddSub();吗?
        • 如果您需要添加不符合该方案的内容,您可以随时添加if (ok) {...}。您可以按照您的建议编写它,但从长远来看,您需要添加一些不适合该方案的代码,所以我更喜欢这样做。任何智能编译器都应该为两者生成相同的代码。
        【解决方案7】:

        简短的回答是:让您的函数返回一个不可能是有效值的错误代码 - 并始终检查返回值。对于返回指针的函数,这是NULL。对于返回非负int 的函数,它是一个负值,通常是-1,以此类推...

        如果每个可能的返回值也是一个有效值,使用call-by-reference:

        int my_atoi(const char *str, int *val)
        {
                // convert str to int
                // store the result in *val
                // return 0 on success, -1 (or any other value except 0) otherwise
        }
        


        检查每个函数的返回值可能看起来很乏味,但这就是在 C 中处理错误的方式。考虑函数nc_dial()。它所做的只是检查其参数的有效性并通过调用 getaddrinfo()、socket()、setsockopt()、bind()/listen() 或 connect() 建立网络连接,最终释放未使用的资源并更新元数据。这可以在大约 15 行中完成。但是,由于错误检查,该函数有近 100 行。但这就是它在 C 中的方式。一旦你习惯了它,你就可以轻松地掩盖你头脑中的错误检查。

        此外,多个if (Action() == 0) return -1; 并没有错。相反:它通常是一个谨慎的程序员的标志。谨慎点就好了。

        作为最后的评论:不要将宏用于定义之外的任何事情,如果有人用枪指着你的头时你无法证明它们的使用是合理的。更具体地说,永远不要在宏中使用控制流语句:这会让你离开公司 5 年后必须维护你的代码的可怜人感到困惑。 if (foo) return -1; 没有任何问题。它简单、干净且显而易见,以至于您无法做得更好。

        一旦你放弃了在宏中隐藏控制流的倾向,就真的没有理由觉得你错过了什么。

        【讨论】:

          【解决方案8】:

          goto 语句是实现异常样式处理的最简单且可能最简洁的方法。如果在宏 args 中包含比较逻辑,使用宏会更容易阅读。如果您组织例程以执行正常(即非错误)工作并且仅在异常上使用 goto,那么阅读起来相当干净。例如:

          /* Exception macro */
          #define TRY_EXIT(Cmd)   { if (!(Cmd)) {goto EXIT;} }
          
          /* My memory allocator */
          char * MyAlloc(int bytes)
          {
              char * pMem = NULL;
          
              /* Must have a size */
              TRY_EXIT( bytes > 0 );
          
              /* Allocation must succeed */
              pMem = (char *)malloc(bytes);
              TRY_EXIT( pMem != NULL );
          
              /* Initialize memory */
              TRY_EXIT( initializeMem(pMem, bytes) != -1 );
          
              /* Success */
              return (pMem);
          
          EXIT:
          
              /* Exception: Cleanup and fail */
              if (pMem != NULL)
                  free(pMem);
          
              return (NULL);
          }
          

          【讨论】:

            【解决方案9】:

            我从来没有想过以这种方式使用gotodo { } while(0) 进行错误处理 - 它非常简洁,但是在考虑之后我意识到在许多情况下我可以通过拆分函数来做同样的事情一分为二:

            int Foo(void)
            {
                // Initialise things that may need to be cleaned up here.
                char* somePtr = malloc(1024);
                if (somePtr = NULL)
                {
                    return NULL;
                }
            
                if (FooInner(somePtr) < 0)
                {
                    // Something went wrong so clean-up.
                    free(somePtr);
                    return NULL;
                }
            
                return somePtr;
            }
            
            int FooInner(char* somePtr)
            {
                if (something(somePtr) < 0) return -1;
                if (somethingElse(somePtr) < 0) return -1;
                // etc
            
                // if you get here everything's ok.
                return 0;
            }
            

            这确实意味着你得到了一个额外的函数,但无论如何我更喜欢许多短函数。

            在 Philips 的建议之后,我也决定避免使用控制流宏 - 只要将它们放在一行上,就会清楚地知道发生了什么。

            至少让我放心的是,我不只是错过了一些东西——其他人也有这个问题! :-)

            【讨论】:

              【解决方案10】:

              使用setjmp

              http://en.wikipedia.org/wiki/Setjmp.h

              http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html

              http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

              #include <setjmp.h>
              #include <stdio.h>
              
              jmp_buf x;
              
              void f()
              {
                  longjmp(x,5); // throw 5;
              }
              
              int main()
              {
                  // output of this program is 5.
              
                  int i = 0;
              
                  if ( (i = setjmp(x)) == 0 )// try{
                  {
                      f();
                  } // } --> end of try{
                  else // catch(i){
                  {
                      switch( i )
                      {
                      case  1:
                      case  2:
                      default: fprintf( stdout, "error code = %d\n", i); break;
                      }
                  } // } --> end of catch(i){
                  return 0;
              }
              

              #include <stdio.h>
              #include <setjmp.h>
              
              #define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
              #define CATCH } else {
              #define ETRY } }while(0)
              #define THROW longjmp(ex_buf__, 1)
              
              int
              main(int argc, char** argv)
              {
                 TRY
                 {
                    printf("In Try Statement\n");
                    THROW;
                    printf("I do not appear\n");
                 }
                 CATCH
                 {
                    printf("Got Exception!\n");
                 }
                 ETRY;
              
                 return 0;
              }
              

              【讨论】:

              • 不,租约不要使用setjmp/longjmp进行错误处理。虽然这段代码看起来简单而好,但在您必须清理资源(如打开的文件)的更复杂的情况下它会失败。
              猜你喜欢
              • 2021-04-06
              • 1970-01-01
              • 2018-08-22
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-01-01
              • 2012-02-01
              相关资源
              最近更新 更多