那么,在实时应用程序的背景下,C++异常的实践情况如何呢?
- 完全不需要吗?
- 甚至可以通过编译器开关关闭吗?
- 还是需要非常小心地使用?
- 或者现在处理得很好,可以几乎自由地使用,只需注意一些事项?
- C++11对此有什么改变吗?
异常现在被很好地处理了,而且用于实现它们的策略使它们实际上比测试返回代码更快,因为它们的成本(以速度衡量)几乎为零,只要您不抛出任何异常。
但是它们确实会花费:在代码大小方面。异常通常与RTTI一起使用,不幸的是,RTTI与其他C++功能不同,在整个项目中启用或禁用,一旦启用,它将为任何具有虚拟方法的类生成补充代码,从而违反“你不为你不使用的付费”思维方式。
此外,它确实需要补充代码来处理。
因此,异常的成本应该根据代码增长而不是速度测量。
编辑:
来自@Space_C0wb0y
:博客文章提供了一个小概述,并介绍了两种广泛使用的实现异常的方法——跳转和零成本。正如名称所示,现在好的编译器使用零成本机制。
维基百科关于异常处理的文章讨论了所使用的两种机制。零成本机制是表驱动机制。
编辑:
来自@Vlad Lazarenko
,他的博客我在上面引用过,可能会因为存在抛出异常而阻止编译器将代码内联和优化到寄存器中。
回答更新部分:
异常处理是否确实需要启用RTTI?
在某些方面,异常处理实际上需要比RTTI和动态转换更强大的功能。考虑以下代码:
try {
some_function_in_another_TU();
} catch (const int &i) {
} catch (const std::logic_error &e) {}
因此,当另一个TU中的函数抛出异常时,它将在堆栈中查找(即立即检查所有级别还是在堆栈展开期间逐个检查每个级别,这取决于实现)与被抛出对象匹配的catch子句。
为了执行此匹配,它可能不需要存储每个对象类型的RTTI方面,因为抛出异常的类型是throw表达式的静态类型。但是它确实需要以instanceof
方式在运行时比较类型,并且需要在运行时执行此操作,因为some_function_in_another_TU
可以从任何地方调用,并带有堆栈上任意类型的catch。与dynamic_cast
不同,它需要对没有虚成员函数的类型以及对于那些不是类类型的类型执行此运行时instanceof检查。最后一部分并不增加难度,因为非类类型没有层次结构,所以只需要类型相等,但仍然需要可以在运行时进行比较的类型标识符。
因此,如果启用异常,则需要RTTI的比较类型的部分,例如dynamic_cast
的类型比较,但覆盖更多类型。您不一定需要存储用于在每个类的vtable中执行此比较的数据的RTTI部分,在那里可以从对象访问该数据 - 该数据可以只在每个throw表达式和每个catch子句的点上进行编码。但我怀疑这不是一个重要的节省,因为typeid
对象并不是非常庞大,它们包含一个通常在符号表中需要的名称,以及一些实现定义的数据来描述类型层次结构。因此,到那时可能最好拥有整个RTTI。
dynamic_cast
不需要RTTI等内容,但我会让它定案并加以梳理:typeid()
的作用,dynamic_cast
的作用,以及vtable
中存储了什么,何时以及如何进行静态类型匹配,以及是否需要这些内容来处理异常。 - towitypeid(object)
,但需要使用 typeid(type)
。 - curiousguy目前并非所有实时环境都支持C++异常处理,这种方法不被普遍接受。
以视频游戏为例(每帧有16.6毫秒的软时间限制),主要编译器实现C++异常处理的方式会使程序运行变慢,增加代码大小,无论是否真正抛出异常。由于游戏主机的性能和内存都至关重要,这是一个致命缺陷:例如,PS3的SPU单元只有256KB的内存用于代码和数据!
此外,抛出异常仍然相当缓慢(如果你不相信,请测量一下),并且可能导致堆分配,这在没有多余微秒的情况下也是不可取的。
唯一的例外是当异常可能在应用程序运行期间仅抛出一次时——不是每帧一次,而是确实只有一次。在这种情况下,结构化异常处理是一种可接受的方式,可以捕获操作系统中的稳定性数据,并在游戏崩溃时将其传递回开发人员。
当抛出异常时,异常机制的实现通常非常缓慢,否则使用它们的成本几乎为零。在我看来,如果你正确使用它们,异常非常有用。
在实时应用程序中,只有当出现问题并且程序必须停止和修复问题(可能需要等待用户交互)时才应该抛出异常。在这种情况下,修复问题需要更长的时间。
异常提供了报告错误的隐藏路径。它们使代码更短,更易读,因此更易于维护。
嵌入式/实时开发通常有3或4个限制 - 特别是当它涉及内核模式开发时
在处理硬件异常时,通常会出现各种限制 - 操作不能抛出更多的硬件异常。C++的隐式数据结构(虚表)和代码(默认构造函数、运算符和其他隐式生成的代码以支持C++异常机制)无法放置,并且因此不能保证在执行此上下文时将其放置在非分页内存中。
代码质量 - 一般情况下,C++代码可以隐藏许多看似微不足道的语句中的复杂性,使得代码难以进行视觉审计以检测错误。异常将处理与位置分离,使得难以证明测试代码覆盖率。
C++公开了一个非常简单的内存模型:new从无限的自由存储器中分配,直到用完为止,并抛出异常。在内存受限的设备上,可以编写更有效的代码,以显式使用固定大小的内存块。C++的隐式分配几乎在任何操作上都使得无法审核内存使用。此外,大多数C++堆具有令人不安的特性,即没有可计算的上限,可以花费多长时间来进行内存分配 - 这再次使得难以证明实时设备上算法的响应时间,其中固定的上限是可取的。
operator new()
来以任何你喜欢的方式分配内存。或者在不适当使用 new
的情况下使用自己的分配器。 - Mike Seymour
new
符合你的要求,小心异常处理(可能需要),等等。 - towi