c++异常实现原理
/>
异常的处理主要包括以下:
抛出异常对应一个信号,相应的回调函数被触发(os层面),回调函数会按照堆栈的反序寻找于此异常关联的处理(也就是对应的catch),如果找到就执行stack unwinding(这里需要维护局部变量的地址表,然后反序调用析构函数),之后执行相应的异常处理逻辑。
异常的空间开销:数据方面,在栈中需要特殊的结构体记录异常处理程序的信息(异常标示、对应的处理函数地址、上一个异常结构地址以及其他体系结构相关的信息),与每个调用相关联的局部变量的地址表(存放在栈以外的某空间)。代码方面,异常抛出后触发的回调函数,函数的逻辑(1)从当前的调用栈向上寻找异常处理过程,找到之后记录下,并执行两个标杆之间的所有局部变量的析构,之后执行catch,把程序流程理顺。如果没找到,调用terminal终止进程。
转自知乎
整个 C++ exception 的行为在常见语言中是最奇葩的, 因为这个语言特性与 C++ 其他 feature(特别是确定性析构) 格格不入。在 C++ 中全面铺开使用异常会遇到其他语言中不存在的问题。
C++ 引入异常的原因之一是为了能让构造函数报错(析构函数不能抛异常这是大家都知道的常识),毕竟构造函数没有返回值,没有异常的话调用方如何得知对象构造是否成功呢?但是编译器/标准库为了让构造函数能抛异常却是麻烦重重:
-
数组元素构造时抛异常,前面已经构造好的元素要析构,还没有构造的元素不能析构。
-
构造函数的初始化列表里抛异常,前面已经构造好的成员和基类子对象要析构,还没有构造的成员则不能析构。而且这个异常捕获之后必须重新抛出(编译器强制),因为C++不允许“半吊子”构造的对象存在。
-
多继承中某个基类的构造函数抛异常,那么已经构造好的基类子对象要析构,还没有构造的基类子对象则不能析构。虚拟继承,虚基类只能析构一次,你慢慢想吧。
-
函数实参对象构造时抛异常,那么多个实参中已经构造好的实参对象要析构,尚未构造的实参对象不能析构。
-
std::vector 在 resizing 的时候某个元素的拷贝发生异常,那么前面已经拷贝的元素要析构,尚未拷贝的元素则不必也不能析构,去看 gcc vector::_M_insert_aux 的代码有多麻烦。
(注脚:C++ 引入异常的另一个原因是让 dynamic_cast<Derived&>(baseReference) 能报错,因为没有 null reference。还有一个原因是让 overloaded operator 能报错,毕竟 operator 的返回类型往往无法包含 error code,例如 operator=() 返回的是 Type&。C++ 也是唯一一个变量赋值有可能会抛异常的语言,例如 Person s; s = getPersonById(someId);,那么即便 getPersonById() 不抛异常也不能保证上一句赋值不抛异常。)
(注脚2:C++ 引入异常的政治原因是 Ada 支持异常,而 Ada 是 DoD 的指定官方语言,如果 C++ 不支持异常,那么 AT&T 贝尔实验室就不能拿 C++ 做 DoD 的项目。)
C++ 编译器要随时提防调用某个函数 foo 会抛异常,这会阻止一些优化,也会产生很多累赘的代码(随时准备析构那些调用 foo 函数前已经构造好的栈上对象)。因此 C++11 的 noexcept 应该大力推广。
C++ 的 exception specification 也很鸡肋,它不像 Java 那样在编译期检查(Java 似乎也流行使用 unchecked exception 了),而是在运行期检查,而且违反的后果是直接终止程序,那谁敢用啊?还不如用代码注释呢。有的编译器干脆就只支持语法而不实现功能(Exception Specifications)。C# 也不支持 exception specification,可见这是一项无用的语言特性,算是编程语言发展历史上走的弯路吧,可惜 Java/C++ 掉坑里了。
其他支持异常的语言几乎都有 GC,抛异常就抛了,不用担心析构,反正GC管着。只有 C++ 才有 exception safety 需要考虑,其他支持异常的语言都没有这一概念。
而且 Java 的 try-with-resource,C# 的 using,Python 的 with 在管理 function local scope 对象的生命期(资源、lock 释放)方面不比 RAII 麻烦。Go defer 要差一些,它是 function 级,不是 block 级,只能对付 return。 不过反正 Go 也没异常,有点小坑罢了,把函数写短点就能绕过。
RAII 的优势在于将对象的生命期管理与其他资源(锁、文件、网络连接等等)的管理整合,然后通过 smart pointers 一并解决了,这是 C++ 独一无二的优势。
如果写递归下降的 parser,那么内部用异常来报错似乎是合理的,对外返回一个 error code 即可。
阅读(1300) | 评论(0) | 转发(1) |