在空指针参数的情况下,最好崩溃还是抛出异常?

3

可能是重复问题:
通过断言还是异常进行设计契约测试?

当将空指针作为输出参数传递给函数时,处理的首选方法是什么?我可以使用ASSERT,但我觉得让库崩溃程序不好。相反,我考虑使用异常。


3
如果您使用非常量引用而不是指针,您就不会遇到这个问题。 :-) - James McNellis
@JamesMcNellis - 或许开发者只是盲目地取消引用了一个NULL指针并调用了未定义的行为。希望运行时会做一些坏事,但对于所有UB,你无法确定。 - R Samuel Klatchko
@RSamuelKlatchko:嗯,对啊。但至少这使得解释为什么调用者有错变得更容易(我个人认为)。 - James McNellis
1
正如其他人所说:C++具有指定非空值(引用)的语言特性。使用它! - adrianm
8个回答

11

抛出异常!那就是它们存在的目的。这样,你库的用户可以决定是否优雅地处理异常或崩溃。

另一个具体的解决方案是返回有效类型的无效值,例如对于返回索引的方法,返回负整数,但只能在特定情况下使用。


@aaa:我的错误,我是指作为索引。谢谢! - Chris Cooper

3
如果不允许使用空指针,则应使用断言。如果对空指针抛出异常,则实际上允许它们作为参数,因为您为这些参数指定了行为。如果不允许空指针但仍然遇到它们,则周围的某些代码肯定存在错误。因此,在我看来,在一些更高级别的地方“处理”它是没有意义的。
要么您希望允许调用者传递空指针并通过抛出异常来处理此情况,并让调用者适当地做出反应(或者让异常传播,如调用者所需),要么您不允许空指针并使用assert进行检查,在发布模式下可能会崩溃(未定义行为),或者使用指定的断言宏,在发布模式下仍然处于活动状态。 strlen等函数采用后一种哲学,而vector<> :: at 等函数采用前一种哲学。后者明确规定了超出范围的值的行为,而前者仅声明传递空指针的行为未定义。
最终,您会如何“处理”空指针呢?
try {
  process(data);
} catch(NullPointerException &e) {
  process(getNonNullData());
}

在我看来,这很丑陋。如果您在函数中断言指针为空,则此类代码将变为:

if(!data) {
  process(getNonNullData());
} else {
  process(data);
}

我认为这种方法更加优越,因为它不使用异常来控制流程(提供一个非空源作为参数)。如果您没有处理异常,那么您可能会在process中失败,并且直接指向崩溃发生的文件和行号(并且使用调试器,您实际上可以获得堆栈跟踪)。

在我的应用程序中,我总是采用assert路线。我的哲学是,空指针参数应该完全由非异常路径处理,或断言为非NULL。


2

两者都要做。

在开发过程中捕获的任何错误都会终止该进程,这将使开发人员明显意识到需要修复它。

如果一个错误在测试中被放行了,仍然有一个异常可以被强大的程序处理。

而这很容易放入宏中(必须是宏而不是内联,以便assert正确报告行号-感谢@RogerPate指出这一点):

#define require_not_null(ptr) \
    do { assert(ptr); if (!(ptr)) throw std::logic_error("null ptr"); } while (0)

将此放入函数中会使assert最有用的方面失效:告诉您表达式、文件名和行号。 - Roger Pate
@RogerPate - 很好的观点。我会将其重写为宏。 - R Samuel Klatchko
请确保将其放在上面链接的副本上,而不是这里。我曾考虑过只发布这样的宏,但我不想处理表达式的多次评估等问题(你最终必须重写assert本身,这并不难,但它不匹配实现的输出格式等)。然而,再次看一下,由于断言表达式中的副作用本来就是不好的,所以这只是一个性能问题。 - Roger Pate

1
如果您注重性能,发布时断言将关闭。它们存在的目的是捕获永远不应该发生的问题,并且不应该用于捕获在现实生活中可能发生的东西。这就是异常的作用。
但让我们退后一步。如果您取消引用空指针,无论是否写入它,都保证会发生什么?它可能会在您的计算机上崩溃,但它不会在每个操作系统、每个编译器或任何其他东西上崩溃。它在您的计算机上崩溃只是您的好运。
我会说,如果您不打算自己创建对象并将指向指针传递给您,则抛出异常,这是我经常看到的“out”参数传递的方式。

1
如果您正在为顶级飞机编写自动驾驶系统,我建议您尝试优雅地处理异常。
请阅读“契约编程”(确实是一种非常好的语言)的 Eiffel 规范,你会有所启发。如果能够处理事件,永远不要崩溃。

1

如果你抛出异常,客户端可以决定重新抛出、不处理异常、崩溃或调用退出或尝试恢复等操作。

如果你崩溃了,客户端也会跟着崩溃。

因此,抛出异常让你的客户端更有灵活性。


1
我既不会引发异常,也不会使用断言,这正是C++标准库所做的。考虑一下库中最简单的函数strlen()。如果它引发了异常,你怎么可能处理它呢?而且在生产代码中,断言不会触发。唯一明智的做法是明确声明该函数不能使用NULL指针作为参数调用,这样做将导致未定义的行为。

1
可能是因为strlen()函数在异常机制之前就已经存在了。如果strlen()函数抛出异常,你可以像处理其他异常一样,使用try catch块来捕获它。 - Puppy
1
我认为我同意Neil的观点,即strlen抛出异常会很奇怪。捕获它的异常有什么意义呢?但是以下语句并不排除断言(也不排除异常,因为未定义的行为可以做任何它想做的事情。但是如果调用代码不知道是否会抛出异常,那么抛出异常就变得毫无意义,因为没有人准备捕获它!):“唯一明智的做法是明确声明该函数不能使用NULL指针作为参数调用,这样做将导致未定义的行为。” - Johannes Schaub - litb
@JohannesSchaub-litb - 异常的一个重要用途是多线程服务器。在请求循环的顶部,您可以使用catch (...)来放弃该请求而不会导致整个服务器崩溃并影响其他请求。现在,如果异常情况确实导致了异常,服务器就可以继续处理所有其他请求而不会崩溃。 - R Samuel Klatchko

0
使用异常的好处在于让客户端代码决定如何处理异常情况。这适用于参数非空是函数的先决条件的情况。然而,对于带有可选输出参数的函数,传递NULL可能表示客户端不感兴趣该值。假设您正在使用返回值来表示成功或失败,如果是这种情况,您可以简单地检测NULL并返回错误代码(如果参数是必需的),或者如果参数是可选的,则可以忽略它。这避免了异常的开销,仍然允许客户端进行错误处理。

在有异常处理机制的编程语言中,现在已经没有人再去检查错误代码了。他们只是默认会抛出异常来解决问题。 - cHao
这可能在Java中是正确的,因为它会在任何时候都抛出异常,但在更少使用异常的语言中(例如Objective-C),检查错误仍然是保持一致性和控制流程的关键。 - warrenm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接