关于库应该如何处理错误或其他异常情况,在不同的软件工程学科中有许多哲学。我见过的几个:
- 使用指针参数返回的结果返回错误代码。这就是 PETSc 所做的。
- 通过标记值返回错误。例如,如果 malloc 无法分配内存,则返回 NULL,
sqrt
如果传入负数,则返回 NaN,等等。这种方法用于许多 libc 函数中。 - 抛出异常。用于deal.II、Trilinos等。
- 返回一个变体类型;例如一个 C++ 函数,
Result
如果它运行正确,则返回一个类型的对象,并使用一个类型Error
来描述它失败的返回方式std::variant<Error, Result>
。 - 使用断言和崩溃。用于 p4est 和 igraph 的某些部分。
每种方法的问题:
- 检查每个错误都会引入大量额外的代码。存储结果的值总是必须首先声明,这会引入许多可能只使用一次的临时变量。这种方法解释了发生了什么错误,但很难确定原因,或者对于深度调用堆栈,很难确定在哪里。
- 错误情况很容易被忽略。最重要的是,如果整个输出类型范围都是合理的结果,那么许多函数甚至都没有有意义的标记值。许多与#1相同的问题。
- 仅适用于 C++、Python 等,而不适用于 C 或 Fortran。可以在 C 中使用 setjmp/longjmp sorcery 或libunwind进行模仿。
- 只能在 C++、Rust、OCaml 等中使用,而不能在 C 或 Fortran 中使用。可以在 C 中使用宏魔法来模仿。
- 可以说是信息量最大的。但是,如果你对一个 C 库采用这种方法,然后为它编写一个 Python 包装器,那么像将越界索引传递给数组这样的愚蠢错误将使 Python 解释器崩溃。
Internet 上关于错误处理的许多建议都是从操作系统、嵌入式开发或 Web 应用程序的角度编写的。崩溃是不可接受的,您必须担心安全性。科学应用几乎没有这些问题,如果有的话。
另一个考虑因素是哪些类型的错误是可恢复的。malloc 失败是不可恢复的,并且在任何情况下,操作系统内存不足杀手会在您之前解决它。超出数组大小范围的索引也无法恢复。对于作为用户的我来说,库可以做的最好的事情就是崩溃并显示信息丰富的错误消息。另一方面,可以通过使用直接分解求解器来恢复迭代线性求解器收敛的失败。
科学图书馆应该如何报告错误并期望它们得到处理? 我当然意识到这取决于库是用什么语言实现的。但据我所知,对于任何足够有用的库,人们都会想用某种语言来调用它,而不是用它实现的语言。
顺便说一句,如果将全局断言处理程序函数指针定义为公共 API 的一部分,我认为方法 #5 可以对 C 库进行实质性改进。断言处理程序将默认报告文件/行号和崩溃。此库的 C++ 绑定将定义一个新的断言处理程序,它会引发 C++ 异常。同样,Python 绑定将定义一个断言处理程序,该处理程序使用 CPython API 来引发 Python 异常。但我不知道有任何采用这种方法的例子。