noexcept与栈展开(stack unwinding)

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之后的点继续执行。

  1. 为局部对象调用析构函数

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

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

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

  1. 异常与构造函数

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

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

不能不处理异常。如果找不到匹配的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主要是给使用者看的,对编译器影响不大

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

  1. 函数指针及该指针指向的函数必须具有一致的异常说明。如果一个指针做出了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。
  2. 如果显示或隐式说明了指针可能抛出异常,那么该指针可以指向任何函数。
  3. 如果一个虚函数承诺不会抛出异常,则后续派生出来的衍生类的虚函数也必须做出同样的承诺。反之则不需要。
  4. 当编译器合成拷贝控制成员的时候,也会生成一个异常说明。如果对所有成员和基类的所有操作都承诺了不抛出异常,则合成的成员是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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值