许多入门程序员往往不能正确地认识错误码与异常的异同,导致他们经常会有一些错误的认知,这些认知阻碍了他们进一步了解异常。而异常是C++标准委员会推荐的错误处理机制,不了解异常,很大程度上意味着你并不了解现代C++。
本文旨在向大家阐明,错误码与异常本质上是一样的东西,都是为了处理错误,只不过是具体写法上有所不同。
首先,我们先广义地来看问题。我们的问题是什么?我们的问题是,当我们的程序执行到某个函数时,这个函数无法完成它所承诺的功能,即,此函数遇到了一个错误。这个错误导致我们无法完成我们想做的事情。
此时,为了保证我们的程序可以继续正常运行,我们需要从这个错误中恢复。所谓恢复意味着:
同时,我们需要对这个错误情况进行处理:
需要注意地是,如果函数f出现了错误,无法完成其所承诺的功能,f的调用者,以及调用者的调用者,一直到某一层调用栈,都会无法完成其所承诺的功能,也即,调用栈的每一层,都需要对此错误进行恢复。但是,如果某一层调用栈,无资源泄漏,也没有破坏程序不变式,错误可以透明地向上报告,不需要额外的恢复动作。
另一方面,当错误传递到某一层函数时,如果此函数的功能并未由于此错误受到影响,或者此错误是此函数预期的,那么,错误传递可以到此为止,我们可以在这一层,将错误消化掉,也即,此错误已经被处理完成。例子如:http服务器的请求循环中,如果某个请求处理失败,请求循环直接返回用户返回一个错误消息即可,如http 404。
当函数遇到一个错误时,我们需要描述此错误,描述此错误的对象,即为错误对象,常见的例子如:
我们会通过这个错误对象来报告我们的错误,并尝试将错误对象向调用者不断传递,直到某个调用者能够处理这个错误。
从错误对象角度来说,错误码与异常对象没有本质的区别,大家可以简单认为,一个错误码,对应一个异常类。错误码用某个值,表示某类错误;异常用某个类,表示某类错误:
异常对象相较于错误码的优点在于:
异常对象的缺点也是有的,但是对能够认识到异常对象的缺点的大佬,应该已经不需要看我的文章了。
当函数执行遇到错误,使之不能完成承诺的功能时,大多数情况下,我们需要终止此函数的执行,但是终止之前,我们需要保证:
因此,我们通过在遇到错误后,执行一些代码,来让程序从错误中恢复。
为了恢复程序的invariant,通常,出错时我们需要手动处理。
其他语言中,经常使用错误处理来管理资源,从而保证错误出现时无资源泄漏。C++中,会使用专门的机制RAII来管理资源,保证资源不会泄漏。因此,在C++中,只要正确使用了RAII,无论使用错误码,还是使用异常处理,都不需要考虑资源泄漏的问题。
函数从错误中恢复后,需要决定,是在本地将错误处理掉,还是上报。显然,当函数f()无法完成承诺的任务时,f的调用者g,也是无法完成自己所承诺的任务的,此时,我们需要将错误报告给g。如此重复,调用栈的每一层,都需要进行恢复,直到某一层能够处理此错误(也即,这一层函数遇到错误,其所承诺的功能还是可以正常完成;或者这一层已经是调用栈的顶层了)。
对于错误码,我们需要手动将错误对象向上传递,如果忘记上报,错误就被静默的丢掉了。它可能导致某一层意识不到已经出错,从而无法执行恢复动作,导致程序在一个错误的状态下越走越远。
对于异常,它可以自动上报:
随着错误对象的不断向上报告,调用栈的每一层都需要进行错误恢复。直到我们到达了某一层:
以上四种情况实际上不完全正交。
对错误处理的角度来看,错误码与异常实际上是没有任何区别的。无论你使用错误码,还是使用异常,你都要决定,具体应该怎么处理当前错误对象。