noexcept与栈展开(stack unwinding)

tech2022-10-21  106

noexcept说明

在C++11之后,表示函数不会抛出异常的动态异常声明throw()被新的noexcept异常声明所取代。 该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。 如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。

从语法上讲,noexcept修饰符有两种形式,一种就是简单地在函数声明后加上noexcept关键字。比如:

void excpt_func() noexcept;

另外一种则可以接受一个常量表达式作为参数,如下所示:

void excpt_func() noexcept (常量表达式);

抛出异常与栈展开(stack unwinding)

抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch。这个过程称为栈展开(stack unwinding)。当处理该异常的catch结束之后,紧接着该catch之后的点继续执行。

为局部对象调用析构函数

如上所述,在栈展开的过程中,会释放局部对象所占用的内存并运行类类型局部对象的析构函数。但需要注意的是,如果一个块通过new动态分配内存,并且在释放该资源之前发生异常,该块因异常而退出,那么在栈展开期间不会释放该资源,编译器不会删除该指针,这样就会造成内存泄露。

析构函数应该从不抛出异常

在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。通常terminate函数将调用abort函数,导致程序的非正常退出。所以析构函数应该从不抛出异常。

异常与构造函数

如果在构造函数对象时发生异常,此时该对象可能只是被部分构造,要保证能够适当的撤销这些已构造的成员。

未捕获的异常将会终止程序

不能不处理异常。如果找不到匹配的catch,程序就会调用库函数terminate。

验证

#include <iostream> using namespace std; struct A { A() { cout << "constructor" << endl; } ~A() { cout << "destructor" << endl; } }; void func1() noexcept { A a1; A a2; } void func2() { A a1; A a2; } int main() { func1(); func2(); return 0; } # gcc -std=c++11 (gdb) disassemble func1 (gdb) disassemble func2

可以看出加了noexcept的函数没有增加_Unwind_Resume调用,汇编代码更短小,为编译优化增加更多可能。

什么时候该使用noexcept

使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率,步子迈大了也容易扯着蛋。 以下情形鼓励使用noexcept:

移动构造函数(move constructor)

移动分配函数(move assignment)

析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。

叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。 最后强调一句,在不是以上情况或者没把握的情况下,不要轻易使用noexception。

每个函数都考虑noexcept会很麻烦,所以只在明显的时候使用

现在编译器在好路径上异常没有影响,noexcept可能的作用是减小体积

推荐在构造、复制等常用操作标记noexcept,这样性能提升可能会比较大。例如vector不会使用你的类move操作,除非它被标记为noexcept(有的编译器能自动推导)

noexcept主要是给使用者看的,对编译器影响不大

异常说明与指针、虚函数和拷贝控制

函数指针及该指针指向的函数必须具有一致的异常说明。如果一个指针做出了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。如果显示或隐式说明了指针可能抛出异常,那么该指针可以指向任何函数。如果一个虚函数承诺不会抛出异常,则后续派生出来的衍生类的虚函数也必须做出同样的承诺。反之则不需要。当编译器合成拷贝控制成员的时候,也会生成一个异常说明。如果对所有成员和基类的所有操作都承诺了不抛出异常,则合成的成员是noexcept的;如果有任意一个函数可能抛出异常,则合成的成员是noexcept(false)。

参考

http://liubigbin.github.io/2016/07/06/C-11%E5%B8%B8%E8%A7%84%E7%89%B9%E6%80%A7%E4%B9%8Bnoexcept/ https://www.jianshu.com/p/08a53d8c9670 https://www.cnblogs.com/zhuyf87/archive/2012/12/23/2829725.html https://www.cnblogs.com/catch/p/3604516.html https://www.cnblogs.com/catch/p/3619379.html https://www.cnblogs.com/sword03/p/10020344.html https://www.zhihu.com/question/30950837 https://stackoverflow.com/questions/10787766/when-should-i-really-use-noexcept

最新回复(0)