【问题标题】:Ways to avoid/lessen the pain of the return value check after every single function call?避免/减轻每次函数调用后返回值检查的痛苦的方法?
【发布时间】:2013-06-27 11:45:13
【问题描述】:

在不支持异常的语言和/或库中,许多/几乎所有函数都返回一个值,指示其操作成功或失败 - 最著名的例子可能是 UN*X 系统调用,例如 open() 或 @ 987654323@,或者一些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 并且在异常中抛出错误,但我不能这样做。下一个最好的事情是什么?我知道there's no real solution to this problem,但我仍然想做一些事情

一些想法:

  • 尝试与assert() 共存(但这不适用于不能直接死掉的生产代码)。
  • 使用宏或返回值检查函数包装函数调用,例如ensure_success(my_function(args)ensure_success(my_other_function(args),my_error_handler,error_handler_args)

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

编辑:

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

【问题讨论】:

  • @sbi 是的,我想我们都同意 C 有“问题”:)
  • 同时,没有人加入我的明智投票,以主要基于意见(在语言战争之前)结束这一决定。
  • 在没有异常处理的语言中,这几乎是您必须做的,以免错过错误。
  • @JimBalter 我现在做到了。
  • assert 不用于错误处理

标签: c error-handling return-value conventions


【解决方案1】:

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

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

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

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

【讨论】:

  • 让我引用您发布的文章:(...)必须自己处理所有可能的错误路径。这是非常痛苦的(...)
  • @BartekBanachewicz 没有痛苦,没有收获:psychologytoday.com/blog/memory-medic/201106/…
  • 明确地说,我同意文章的结论:“我不建议使用 C,除非对决定进行了深思熟虑”。我想补充一点:C 不会产生可靠的软件,它会产生可靠的程序员。
  • @BartekBanachewicz 这确实是另一种选择。
【解决方案2】:

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

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

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

等等。

【讨论】:

    【解决方案3】:

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

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

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

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


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

    • 简单易懂
    • 足够严格以提高效率
    • 足够灵活,可以扩展
    • 保存失败(此处不再赘述)
    • 系统/一致(在模式、命名、布局方面)
    • 记录在案

    可能使用的语言结构:

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

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

    【讨论】:

    • 使用longjmp是自找麻烦。
    • @CatPlusPlus 如果你称自己为大师,你应该能够处理这个...... - 但你是对的,我不喜欢使用它。压力太大。但我忍不住要提它,听了你上面的战争...... ;-)
    • longjmpgoto 可以使用,如果它是一个成熟和有纪律的编码器.. ;-)
    • @alk 甚至longjmp 的手册页都说这很糟糕,应该避免。编码器的成熟度与它背后的整个想法很糟糕的事实绝对无关
    • @BartekBanachewicz:我不同意 - longjmp() 填补了状态码/errnoexit()/abort() 之间的空白,并且在涉及回调时变得特别有用
    【解决方案4】:

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

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

    大致上,您可以将错误情况分为致命和非致命两种情况。

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

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

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

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

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

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

    _Bool do_stuff(int i);
    do_stuff(i) || panic("failed to do stuff with %i", i);
    

    【讨论】:

      【解决方案5】:

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

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

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

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

      【讨论】:

      • 好的,这是一个选项,但我尽量避免将调用放在 if 语句中;我发现这有点令人困惑。
      • 唉,这排除了很多可能性。你可以宏观化它。另外,我会因为这条评论而被枪杀,但有时goto 也有帮助。
      • 没有人应该反对你提到 goto ...它是现代结构化汇编语言(如 C)的现代特性。
      • 那么,您能否扩展您的答案以建议基于goto 的方法?
      • @DeadMG 你可能在这里错过了之前的(mod-deleted)讨论。
      【解决方案6】:

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

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

      【讨论】:

      • 我不确定这如何解决我的问题......除非你建议我用这种异常持有者编写包装器?
      【解决方案7】:

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

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

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

      【讨论】:

      • 我不是它的忠实拥护者,但 Go 对这个问题有一种严谨的方法,它不基于异常(除了一些非常有限的形式)。
      猜你喜欢
      • 2017-01-24
      • 1970-01-01
      • 1970-01-01
      • 2015-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-02
      相关资源
      最近更新 更多