✏️

Exception

Tags
C++的异常可以是任意类型,throw和catch块都可以处理任意类型的异常,但最好是继承于std:exception 的派生类
标准异常可以分为std::runtime_error std::logic_error 的子类 以及 其他标准库的异常,如bad_alloc , bad_cast
 

异常处理基础过程

在try-catch块throw一个异常e时,C++的处理步骤:
  • 终止函数调用链,后面的语句不会再执行
  • 编译器在特殊内存(一般不是栈)上创建抛出的对象副本e
  • 栈展开(stack unwinding),沿着函数调用栈回溯,销毁栈内对象(调用其析构方法,然后把栈压缩回去)
    • 这也是为什么建议在析构函数中释放资源
  • 寻找第一个与e匹配的catch块,若无则向上抛出
  • 若找不到匹配的catch块则调用std::abort 终止程序
  • 在最后一个catch块结束后,销毁异常对象副本
 
💡
不要在析构函数中抛出异常。C++不接受多个活跃异常同时存在(即不能在throw之后 ~ catch处理的这个阶段检测到异常),例如:
#include <iostream> class Test { public: Test() {} ~Test() { throw 24; } }; int main() { try { Test t; throw 8; } catch (int n) { std::cout << n; } return 0; }
在这种情况下,stack unwinding会调用t的析构函数,会直接调用std::terminal 终止运行
 

异常对象

必须是一个完全类型(已声明和定义)
不能是右值引用
如果是类类型则必须含有一个可访问的析构函数 与 一个可访问的拷贝or移动构造函数
显然抛出一个指向局部变量的指针是UB
// 虽然实测正常,但其实仍然是个UB,因为x处的内存被编译器“销毁”了但是没被复写,还能访问 int* exception_ptr = nullptr; try { int x = 1; throw &x; } catch (int* p) { exception_ptr = p; } // ... // 有概率报错!访问这片区域实则UB std::cout << *exception_ptr << std::endl;
 
💡
抛出的表达式的类型由其静态编译时类型确认 e.g.
Base* ptr = new Derived(); throw *ptr; // 抛出的对象是Base而非Derived,被捕捉之后就不能调用派生类only的操作了
 

noexcept

在函数声明时,在参数列表之后标记noexcept关键字,告诉编译器这个函数不会抛出异常,便于编译器优化
也可以用noexcept操作符判断一个函数会不会抛出异常
template<typename F> void call(F f) { if constexpr (noexcept(f())) { f(); // 使用优化路径 } else { try { f(); } catch(...) { /* 处理异常 */ } } }