7.5 回归基础
从根本上讲,我认为 C++ 需要两种错误处理机制:
- 异常——罕见的错误或直接调用者无法处理的错误。
- 错误码——错误码表示可以由直接调用者处理的错误(通常隐藏在易于使用的检测操作中或作为 (值,错误码) 对从函数返回)。
考虑代码:
void user()
{
vector<string> v{"hello!"};
for (string s; cin>>s; )
v.push_back(s);
auto ps = make_unique<Shape>(read_shape(cin));
Smiley_face face{Point{0,0},20};
// ...
}
这个例子是人造的,但其编程风格并非不典型。我们可以从中看出,user()
函数里有很多发生不太可能的错误的可能性:内存耗尽、读取错误、构造失败(例如,在 Smile_face
的多层次结构中出现错误等)。另外,使用 unique_ptr<Shape>
可以防止内存泄漏。如果我们使用显式错误码而不是异常,那么这个函数中至少需要进行五次错误检查,源代码数量将翻倍,并需要在各个构造函数中进行更多检 查。没有 RAII(及其与异常的集成),代码将进一步膨胀。一般来说,更多的代码意味着更多的错误。当添加的代码使控制流程复杂时,尤其如此。这一点经常被那些通 过小例子论证的人所忽视。对于小例子来说,就一项测试
关系不大,相对也很难漏掉。
另一方面,有些错误是预料得到的,我们更愿意使用某种形式的错误码来对其进行检查:
ifstream f {"Myfile"};
if (!f) {
// ... 处理错误 ...
}
// ... 使用 f ...
在这里,为方便起见,错误码隐藏在输入流的状态里。
因此,在理想情况下,应该只有两种错误处理的方法,但是我真的不知道如何达到这样一种理想状态。仅仅 (值,错误码) 对就有十几种变体被广泛使用(例如 std::map::insert()
), 并且还有一些新的变体也在 2011 年的 WG21 中被讨论(如 [Botet and Bastien 2018; Sutter 2018b])。即使委员会能就其中一个方案达成一致,也仍然会有至少十几个广泛使用的错误处理方案,每个方案都有一大群忠实的追随者支持,许多方案都有 数百万行难以更动的代码。
很少有关于异常的性能和 C++ 中返回码可靠性的认真研究([Renwick et al. 2019] 是一个例外)。但是,有许多不科学的小研究和许多大声表达的意见——常常声称异常天生就比各种形式的错误码检查慢。这与我的经验不符。就我所知,还没有任 何严谨的研究发现在现实的例子中错误码能胜出很多
,或者异常能胜出很多
。在这一讨论场景下,很多
表示整数倍的差异,而不是几个百分点。
运行一个简单的性能测试:进行一个 N 层深度的调用序列,然后报告错误。如果错误很少见,例如 1:1000 或 1:10000 的错误率,并且调用嵌套很深,例如 100 或 1000,则异常处理要比明确的错误码判断方式快得多。如果调用深度为 1,并且错误发生的概率为 50%,则显式判断错误码测试将大获全胜。调用深度和错误概率决定了这些测试之间的差异。我要问一个简单而潜在有用的问题:一个错误要多罕见才被看作是异常情况
?不幸的是,答案是这要看情况
。这取决于代码、硬件、优化器、异常处理的实现,等等等等。C++ 异常的设计假设答案至少在 1:100 的范围。换句话说,错误指示的传播要远比显式的处理更为常见。
空间占用问题可能比运行期问题更难解决。对于那些遇到不能在本地处理的错误就可以立即终止的系统,我可以想象这样一个实现,在遇到 throw
时立即终止程序。但是如果要传播和处理错误,那么就不可避免,需要面对选择各种困难的折中。
对于错误处理这团乱码,任何解决方案都很可能遇到 N+1 问题(§4.2.5)[Stroustrup 2018a]。
奇怪的是,当初 C++ 引入异常时,人们担心的问题之一就是异常不够通用。许多人认为恢复(resumption)语义必不可少 [Stroustrup 1993]。当时我的猜测是,允许恢复将使异常处理的速度至少再降低两倍。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论