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


11

在不同的软件工程学科中,有很多关于库应如何处理错误或其他特殊情况的哲学。我见过的一些:

  1. 返回一个错误代码,其结果由指针参数返回。这就是PETSc所做的。
  2. 通过哨兵值返回错误。例如,如果malloc无法分配内存,sqrt则返回NULL;如果传递负数,则将返回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。可以使用setjmp / longjmp巫术或libunwind在C中进行模仿。
  4. 仅在C ++,Rust,OCaml等中可用,而在C或Fortran中则不可用。可以使用宏魔术在C语言中模仿。
  5. 可以说是最有用的。但是,如果将这种方法用于一个C库,然后为该C库编写Python包装器,那么一个愚蠢的错误(例如将越界索引传递给数组)将使Python解释器崩溃。

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

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

科学图书馆应如何报告错误并期望对其进行处理? 我当然知道这取决于实现该库所使用的语言。但是据我所知,对于任何足够有用的库,人们都希望从某种语言(而不是其所使用的一种语言)来调用它。

顺便说一句,我认为如果C语言库将全局声明处理程序函数指针定义为公共API的一部分,则可以大大改善C语言库的方法#5。断言处理程序将默认报告文件/行号并崩溃。该库的C ++绑定将定义一个新的断言处理程序,该处理程序将引发C ++异常。同样,Python绑定将定义一个声明处理程序,该处理程序使用CPython API引发Python异常。但我不知道采用这种方法的任何示例。


另一个考虑因素是性能影响。这些各种方法如何影响软件的速度?我们应该在代码的“控制”部分(例如处理输入文件)中使用与计算上昂贵的“引擎”不同的错误处理吗?
LedHead

请注意,最佳答案因语言而异。
克莱里斯

Answers:


10

我将为您提供我的观点,该观点编码在您引用的deal.II项目中。

首先,有两种错误条件:可以从中恢复的错误和不能从中恢复的错误。

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

  • 例如,后者是,如果您尝试将大小为10的向量添加到大小为20的向量中:请尝试一下,对此无能为力-代码中的错误导致我们尝试进行加法的点。

无论您使用哪种编程语言,都应该对这两个条件进行不同的对待:

  • 在第二种情况下,由于没有追索权,请终止该程序。您可以通过引发异常或返回错误代码来指示调用者无法执行任何操作来做到这一点,但是您最好立即终止程序,因为这使程序员更容易调试问题。

  • 在前一种情况下,出现了可以解决的特殊情况。即使C和Fortran无法表达这一点,但后来出现的所有合理的语言都已经通过提供“例外”的方式将方法合并到语言标准中,以处理此类“例外”回报。使用这些-这就是它们的作用;它们的设计方式也使您不会忘记忽略它们(如果这样做,则异常只会传播一个级别)。

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

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


我只建议相反:在无法处理情况时引发异常,在可以处理情况时返回错误代码。问题在于处理抛出的异常很棘手:应用程序程序员必须知道所有可能的异常的类型才能捕获并处理它们,否则程序将崩溃。对于无法处理的情况,崩溃是可以接受的,甚至是受欢迎的,因为例如使用python开箱即用地报告了崩溃点,但是对于可以处理的情况,(大多数)不受欢迎。
cdalitz

@cdalitz:这是C ++的设计缺陷,您可以抛出任何类型的对象。但是任何合理的软件(不包括Trilinos)都只会引发从派生的异常std::exception,并且可以通过引用捕获这些异常,而无需知道派生的类型。
Wolfgang Bangerth

1
但是由于原始问题中概述的原因,我强烈不同意返回错误代码:(i)错误代码经常被忽略,因此根本不处理错误;(ii)在许多情况下,鉴于函数的返回类型是固定的,根本就没有可以合理地返回的异常值;(iii)函数具有不同的返回类型,并且在每种情况下,您都必须分别定义代表错误的“例外”值。
Wolfgang Bangerth

WB写道(很抱歉,由于某种原因'@'技巧不起作用,并且由于某种原因,用户名被StackExchage删除):“错误代码经常被忽略”。对于异常捕获而言,这更为重要:很少有软件开发人员会把将每个函数调用放在try / catch块中的麻烦。但这主要是一个品味问题:只要文档中清楚说明了函数是否抛出异常以及哪些异常引发,我就可以处理。但还是可以这样说:编写文档的职责经常被忽略;-)
cdalitz 19/09/18

但是关键是,如果您忘记捕获异常,那么就不会出现下游问题:该程序只会中止。查找问题发生的位置将很容易。如果您忘记检查错误代码,则由于未定义的内部状态,程序可能在以后的某个时间崩溃-但最初的问题所在仍然完全不清楚。很难找到这些错误。
Wolfgang Bangerth
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.