科学图书馆应该如何报告错误?

计算科学 软件
2021-12-02 04:04:13

关于库应该如何处理错误或其他异常情况,在不同的软件工程学科中有许多哲学。我见过的几个:

  1. 使用指针参数返回的结果返回错误代码。这就是 PETSc 所做的。
  2. 通过标记值返回错误。例如,如果 malloc 无法分配内存,则返回 NULL,sqrt如果传入负数,则返回 NaN,等等。这种方法用于许多 libc 函数中。
  3. 抛出异常。用于deal.II、Trilinos等。
  4. 返回一个变体类型;例如一个 C++ 函数,Result如果它运行正确,则返回一个类型的对象,并使用一个类型Error来描述它失败的返回方式std::variant<Error, Result>
  5. 使用断言和崩溃。用于 p4est 和 igraph 的某些部分。

每种方法的问题:

  1. 检查每个错误都会引入大量额外的代码。存储结果的值总是必须首先声明,这会引入许多可能只使用一次的临时变量。这种方法解释了发生了什么错误,但很难确定原因,或者对于深度调用堆栈,很难确定在哪里。
  2. 错误情况很容易被忽略。最重要的是,如果整个输出类型范围都是合理的结果,那么许多函数甚至都没有有意义的标记值。许多与#1相同的问题。
  3. 仅适用于 C++、Python 等,而不适用于 C 或 Fortran。可以在 C 中使用 setjmp/longjmp sorcery 或libunwind进行模仿。
  4. 只能在 C++、Rust、OCaml 等中使用,而不能在 C 或 Fortran 中使用。可以在 C 中使用宏魔法来模仿。
  5. 可以说是信息量最大的。但是,如果你对一个 C 库采用这种方法,然后为它编写一个 Python 包装器,那么像将越界索引传递给数组这样的愚蠢错误将使 Python 解释器崩溃。

Internet 上关于错误处理的许多建议都是从操作系统、嵌入式开发或 Web 应用程序的角度编写的。崩溃是不可接受的,您必须担心安全性。科学应用几乎没有这些问题,如果有的话。

另一个考虑因素是哪些类型的错误是可恢复的。malloc 失败是不可恢复的,并且在任何情况下,操作系统内存不足杀手会在您之前解决它。超出数组大小范围的索引也无法恢复。对于作为用户的我来说,库可以做的最好的事情就是崩溃并显示信息丰富的错误消息。另一方面,可以通过使用直接分解求解器来恢复迭代线性求解器收敛的失败。

科学图书馆应该如何报告错误并期望它们得到处理? 我当然意识到这取决于库是用什么语言实现的。但据我所知,对于任何足够有用的库,人们都会想用某种语言来调用它,而不是用它实现的语言。

顺便说一句,如果将全局断言处理程序函数指针定义为公共 API 的一部分,我认为方法 #5 可以对 C 库进行实质性改进。断言处理程序将默认报告文件/行号和崩溃。此库的 C++ 绑定将定义一个新的断言处理程序,它会引发 C++ 异常。同样,Python 绑定将定义一个断言处理程序,该处理程序使用 CPython API 来引发 Python 异常。但我不知道有任何采用这种方法的例子。

1个回答

我会给你我的观点,它被编码在你引用的 deal.II 项目中。

首先,有两种错误情况:可以恢复的错误和无法恢复的错误。

  • 前者是,例如,如果无法读取输入文件 - 例如,如果您正在从$HOME/.dealii可能存在或不存在的文件中读取信息。读取函数应该只返回到调用函数,以便后者弄清楚要做什么。也可能是某个资源目前不可用,但可能在一分钟后再次可用(远程安装的文件系统)。

  • 后者是,例如,如果您尝试将大小为 10 的向量添加到大小为 20 的向量:尽您所能尝试,对此无能为力 - 代码中存在导致我们试图做加法的地方。

无论您使用哪种编程语言,都应区别对待这两个条件:

  • 在第二种情况下,由于没有追索权,终止程序。您可以通过抛出异常或返回错误代码来向调用者指示无法执行任何操作,但您也可以立即中止程序,因为这使程序员更容易调试问题。

  • 在前一种情况下,出现了可以处理的异常情况。尽管 C 和 Fortran 无法表达这一点,但后来出现的所有合理语言都将方法纳入语言标准,通过提供“异常”来处理这种“异常”返回。使用这些——这就是它们的用途;它们的设计方式也使您不会忘记忽略它们(如果这样做,异常只会传播更高的级别)。

换句话说,我在这里提倡的(以及 deal.II 所做的)是您的策略 3 和 5 的混合,具体取决于上下文。确实,3 在 C 或 Fortran 之类的语言中不起作用——在这种情况下,人们可能会争辩说,这是一个很好的理由,不要使用那些难以表达你想做的事情的语言。

我会注意到一些系统不应该崩溃,即使在错误不可恢复的情况下。一个示例是针对多个查询重复调用一组函数 - 例如,用于评估统计抽样方案中给定输入的似然函数。也许评估者无法处理负值,因为在这种情况下问题没有任何意义(例如,评估厚度金属板的刚度)x),但由于需要反复调用评估器,它不应该只是崩溃,而是抛出一个异常。在这种情况下,即使传入负值是不可恢复的,也应该抛出异常而不是中止程序。几年前我不同意这种立场,但是在 xSDK 社区软件指南编码了程序永远不应该崩溃的要求(或者至少应该有一种从崩溃到异常切换的方法——所以交易之后,我改变了主意。 II 现在可以选择Assert抛出异常而不是调用abort()。)