有没有更好的方法来进行C风格的错误处理?

时间:2011-04-15 13:15:57

标签: c error-handling

我正在尝试通过编写一个简单的解析器/编译器来学习C语言。到目前为止它是一个非常有启发性的经验,但是来自C#的强大背景我调整了一些问题 - 特别是缺乏例外。

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

目前我正在编写需要快速失败的代码,如果有问题,并且它也可能深度嵌套 - 我已经确定了错误处理模式,其中“获取”功能在错误上返回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
  • 某些函数在错误
  • 上返回< 0
  • 某些功能永远不会产生错误
  • 我的函数执行NS_SetError(),但许多其他函数没有。{/ li>

有没有更好的方法可以构建我的函数,还是其他所有人都有这个问题?

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

10 个答案:

答案 0 :(得分:13)

当您必须在每个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;

答案 1 :(得分:6)

一种清理技术是使用一个永远不会实际迭代的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可以打破任何其他方法,但这不是其中之一。

答案 2 :(得分:4)

你可能不想听到这个,但C方式做异常是via the goto statement。这是该语言的原因之一。

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

所以现在听起来像你现在是时候用语言语法元素goto来表达自己的时间了。

答案 3 :(得分:3)

至少必须考虑两个层面:你的功能如何相互作用,以及它在断裂时的作用。

我看到始终的大多数大型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;

答案 4 :(得分:3)

除了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 :(得分:3)

简短的回答是:让您的函数返回一个不可能是有效值的错误代码 - 并始终检查返回值。对于返回指针的函数,这是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;没有错。它简单,干净,显而易见,你不能做得更好。

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

答案 6 :(得分:2)

这个怎么样?

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

答案 7 :(得分:2)

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

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

答案 8 :(得分:1)

我从来没有想过以这种方式使用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;
}

现在这意味着您可以获得额外的功能,但无论如何我的偏好都是针对许多短功能。

在飞利浦的建议之后,我也决定避免使用控制流宏 - 只要你将它们放在一行就足够清楚了。

至少它让人放心,我不会错过任何东西 - 其他人也有这个问题! : - )

答案 9 :(得分:0)

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