10

在不支持异常的语言和/或库中,许多/几乎所有函数都返回一个指示其操作成功或失败的值——最著名的例子可能是 UN*X 系统调用,例如open()or chdir(),或一些 libc 函数。

无论如何,当我编写 C 代码时,它通常最终看起来像这样:

int retval;
...
retval = my_function(arg1, arg2);
if (retval != SUCCESS_VALUE) { do_something(); }

retval = my_other_function(arg1, arg2);
if (retval != SUCCESS_VALUE) { do_something_else(); }

现在,我想要的是不要在任何地方保存 retval 并且在异常中抛出错误,但我不能这样做。下一个最好的事情是什么?我知道这个问题没有真正的解决方案,但我还是想做点什么

一些想法:

  • 尝试与assert()'s 一起生活(但这不适用于无法承受死亡的生产代码)。
  • 使用宏或返回值检查函数包装函数调用,例如ensure_success(my_function(args)ensure_success(my_other_function(args),my_error_handler,error_handler_args).

在这件事上我可能更喜欢其他任何做法吗?

编辑:

  • 是的,我正在编写 C 代码。我尊重您的意见,即我应该尽量避免完全用 C 语言编写,但这确实没有建设性。这不是一个语言战争问题,请不要把它变成一个问题。
  • 我不是在征求关于什么是最好的事情的意见,我只是想要更多的可能性。(我会选择我喜欢的,其他人可能会选择其他的。)
4

7 回答 7

3

您面临的事实是,在许多问题的解决方案中,很多事情都可能出错。

处理这种“错误”情况是解决问题的一部分。

所以我的答案是要么坚持解决没有任何(或很少)可能失败的问题,要么承担接受处理错误条件是解决方案的一部分的负担。

因此(也)设计/编写处理失败所需的代码是成功的重要途径。


至于实际建议:将错误处理视为整个代码的一部分,对于错误处理,相同的规则适用于每一行代码:

  • 容易明白
  • 足够严格以提高效率
  • 足够灵活,可以扩展
  • 保存失败(在此上下文中重复提及)
  • 系统/一致(在模式、命名、布局方面)
  • 记录在案

可能使用的语言结构:

  • break脱离当地环境
  • ( goto)*
  • longjmp和朋友“模拟”异常)*
  • 清晰定义(再次系统化/一致)的函数声明和返回码

* 以结构化的方式与纪律一起使用。

于 2013-06-27T11:59:37.247 回答
3

尝试以一种科学的好奇心来解决这个问题。许多人声称 C 的错误处理方法使程序员更加了解错误情况,更加关注错误以及应该在哪里/如何处理错误。只需将其视为一种意识练习(如果有点乏味),例如冥想:)

不要与之抗争。尽可能本着 C 的精神解决这个问题,你对事物的看法就会稍微扩大。

查看这篇关于 C 错误处理咒语的文章:http: //tratt.net/laurie/tech_articles/articles/how_can_c_programs_be_so_reliable

对您的一般问题的一个一般答案是:尝试使函数尽可能小,以便您可以在错误时直接从它们返回。这种方法适用于所有语言。剩下的就是结构化代码的练习。

于 2013-06-27T12:25:56.197 回答
3

有很多方法可以实现更好的错误处理。这完全取决于你想要做什么。仅仅为了调用几个函数而实现广泛的错误处理例程是不值得大惊小怪的。在较大的代码库中,您可以考虑它。

更好的错误处理通常是通过添加更多的抽象层来实现的。例如,如果您有这些功能

int func1 (int arg1, int arg2)
{
  return arg1 == arg2;
}

int func12 (int arg1, int arg2)
{
  return arg1 - arg2;
}

和一些错误处理函数:

void err_handler_func1 (int err_code)
{
  if(err_code != 0)
  {
    halt_and_catch_fire();
  }
}

然后您可以将函数和错误处理程序组合在一起。通过创建包含一个函数和一个错误处理程序的基于结构的数据类型,然后创建一个此类结构的数组。或者通过使用索引访问单个相关数组:

typedef void(*func_t)(int, int);
typedef void(*err_handler_t)(int);

typedef enum
{
  FUNC1,
  FUNC2,
  ...
  FUNC_N // not a function name, but the number of items in the enum
} func_name_t;

const func_t function [FUNC_N] =
{
  func1,
  func2,
  ...
};

const err_handler_t err_handler [FUNC_N] = 
{
  err_handler_func,
  err_handler_func,
  ...
}

一旦你有了这个,你可以将函数调用包装在一个合适的抽象层中:

void execute_function (int func_n, int arg1, int arg2)
{
  err_handler[func_n]( function[func_n](arg1, arg2 );
}

execute_function (FUNC1, 1, 2);
execute_function (FUNC2, 2, 2);

等等。

于 2013-06-27T12:31:16.160 回答
2

与许多事情一样,C 中的错误处理比其他(高级)语言需要更多的关注。

就个人而言,我认为这不一定是一件坏事,因为它会迫使您实际考虑错误条件、相关的控制流程,并且您需要提出适当的设计(因为如果您不这样做,结果可能会成为无法维护的混乱)。

粗略地说,您可以将错误情况分为致命和非致命两种情况。

在非致命的情况下,有一些是可恢复的,即您只需再试一次或使用回退机制。那些,您通常处理内联,即使在支持异常的语言中也是如此。

然后,有些是您无法恢复的。相反,您可能想要记录失败,但通常只通知调用者,例如通过返回码或某些errno类型变量。可能,您可能需要在函数内进行一些清理,其中 agoto可能有助于更清晰地构建代码。

在致命的情况下,有一些会终止程序,即您只是打印一些错误消息并且exit()状态为非零。

然后,有一些例外情况,您只需转储核心(例如 via abort())。断言失败是其中的一个子集,但你应该只assert()在正常执行期间不可能出现这种情况时使用,这样你的代码仍然在NDEBUG构建时完全失败。

第三类致命异常是那些不会终止整个程序,而只是(可能是深度嵌套的)调用链的异常。您可以longjmp()在此处使用,但必须注意正确清理分配的内存或文件描述符等资源,这意味着您需要跟踪某些池中的这些资源。

一些可变参数宏魔术可以为至少其中一些情况提出很好的语法,例如

_Bool do_stuff(int i);
do_stuff(i) || panic("failed to do stuff with %i", i);
于 2013-06-27T12:28:58.437 回答
0

&&在罕见但快乐的情况下,您可以利用和的短路特性||

if (my_function1(arg1, arg2) == SUCCESS_VALUE 
 && my_function2(arg3) == SUCCESS_VALUE)
{
  /* Proceed */
}
else
{
  /* Cry and die. */
}

如果您必须区分函数 1 和 2 中的错误,这可能效果不佳。

从美学上讲,不是超级漂亮,但这毕竟是错误处理:)

于 2013-06-27T11:54:35.700 回答
0

许多 glib API 都有一个GError抽象,它充当某种(当然,这是 C 语言)异常容器。或者也许errno抽象是一个更好的描述。

它用作指向指针的指针,允许您快速检查调用是否成功(将创建一个新的 GError 来保存任何错误)并同时处理详细的错误信息。

于 2013-06-27T12:16:07.940 回答
0

如果在 C 中有解决方案,那么就不会发明异常。你只有两个选择:

  • 转移到支持异常的语言。
  • 为任何迫使您使用 C 和编写 C 的因素而哭泣,所有这些都是无法解决的问题。

虽然,如果您使用 Visual C,则可以在 C 中将 SEH 用作异常。它并不漂亮,但确实有效。

于 2013-06-27T12:21:41.070 回答