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

Jus*_*tin 11 c error-handling

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

现在我已经阅读了Cleaner,更优雅,更难以识别,我同意该文章中的每一个字; 在我的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;
}
Run Code Online (Sandbox Code Playgroud)

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

总的来说,我觉得我错过了一些东西 - 尽管这种方式的错误处理应该更容易识别,在我的许多功能中,我真的很难确定是否正确处理错误:

  • 某些函数会返回NULL错误
  • 某些函数会返回< 0错误
  • 某些功能永远不会产生错误
  • 我的函数做了一个NS_SetError(),但许多其他函数没有.

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

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

Bla*_*iev 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;
}
Run Code Online (Sandbox Code Playgroud)

你甚至可以在这周围创建自己的宏来为你节省一些打字,就像这样(我还没有测试过):

#define CALL(funcname, ...) \
  if (funcname(__VA_ARGS__) < 0) { \ 
    goto failure_ ## funcname; \
  }
Run Code Online (Sandbox Code Playgroud)

总的来说,它比一般的处理更清洁,更少冗余:

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;
}
Run Code Online (Sandbox Code Playgroud)

作为一个额外的提示,我经常使用链接来减少if代码中的数量:

if (a() < 0 || b() < 0 || c() < 0) {
  return FAILURE;
}
Run Code Online (Sandbox Code Playgroud)

由于||是短路运营商,上述将取代三个独立if的运营商.考虑在return语句中使用链接:

return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;
Run Code Online (Sandbox Code Playgroud)


Jam*_*mes 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;
Run Code Online (Sandbox Code Playgroud)

你失去了一定程度的缩进.

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

  • @Kragen用另一种方式说:"类似于10,但表示为1 + 2 + 3 + 4"("只是没有10").+1,我也喜欢这个想法,但我不喜欢你认为它更好"因为它没有goto". (5认同)
  • 如果您有多个可能的错误条件,一些共享清理而另一些不清除怎么办?这将在简单的情况下工作,但即便如此,对于一个相当简单(并且可以说更清晰)的goto来说,这似乎是一个相当迟钝的解决方法. (3认同)
  • 使用while循环的"break"功能可以直接进入需要清理的位置.谁可以下来投票,请解释原因? (2认同)
  • 如果您需要在循环或其他`while`中进行错误检查,此解决方案将失败.宏的中断将突破循环/而不是外部的`while(0)`. (2认同)

T.E*_*.D. 5

您可能不会喜欢听到这个,但是 C 的异常处理方式是通过gotostatement。这是它在语言中的原因之一。

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

所以在我看来,现在是时候让您熟悉那些语言语法元素的goto.

  • @James - 是的,有。然而,随着事情变得复杂,这些方式变得越来越尴尬。在某些时候,你最好承认你的控制流是非结构化的,而不是试图进一步掩盖它,这样你就可以假装它不是。 (4认同)