跳转至

一、基础用法

void func1(void)
{
    throw "testError";   // 抛出 const char*
}
void func2(void)
{
    func1();
}

int main()
{
    try {
        func2();
    } catch(const char* s) {   // 与 throw 的 const char* 类型匹配
        cout << "catch error:" << s << endl;
    }
    catch(...){
        // 任意类型异常的兜底处理
    }
    return 0;
}

标准与实现

main 的返回类型标准规定为 int([basic.start.main])。void main 非标准——MSVC 作为扩展接受,严格而言是 ill-formed,gcc/clang 在托管环境会警告或拒绝。另外 catch 的类型匹配不做 const char*std::string 这类用户定义转换(仅允许数组→指针、函数→指针、限定符调整、指针到 void*、基类↔派生类指针等有限转换):throw "..." 抛的是 const char*,只能被 catch(const char*)catch(...) 捕获,catch(const string&) 接不住。

二、异常体系

cpp_8-异常_260718_160132.png

头文件 #include <stdexcept>

2.1 继承

三、使用注意事项

3.1 优缺点来源

优点:(相对于错误码)

  • 异常可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用等信息,这样可以帮助更好的定位程序的bug。
  • 避免层层返回错误码,代码冗余,不好看。
  • 很多的第三方库都会使用异常,比如 boost、gtest、gmock 等常用的库,如果我们不用异常就不能很好的发挥这些库的作用,很多测试框架也都使用异常,因此使用异常能更好的使用单元测试等进行白盒测试。
  • 部分函数使用异常更好处理,比如 T& operator 这样的函数,如果 pos 越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

缺点:

  • 异常会导致程序的执行流混乱,这会导致我们跟踪调试或分析程序时比较困难。异常还会有一些性能的开销,当然在现代硬件速度很快的情况下,这个影响基本忽略不计!
  • C++ 没有垃圾回收机制,资源需要自己管理,有了异常非常容易导致内存泄露、死锁等异常安全问题,这个需要使用 RAII 来处理资源的管理问题,学习成本比较高
  • C++ 标准库的异常体系定义得不够好,导致大家各自定义自己的异常体系,非常的混乱,异常尽量规范使用,否则后果不堪设想,随意抛异常,也会让外层捕获的用户苦不堪言。
  • 异常接口声明不是强制的,对于没有声明异常类型的函数,无法预知该函数是否会抛出异常

3.2 栈自旋

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋( unwinding)。而堆上的空间,则会泄漏。

异常的抛出可以认为是特殊的return,因此会打乱程序的流程。同return,要注意newdelete要成对。

别让异常逃离析构函数(条款08)

  • 首先应尽量避免在析构函数中出现异常
  • 若为了避免用户忘记 close 操作:双重保险方案——判断用户没有 close,则调用 close 函数,并在析构 catch 中记录下来,并调用 abort 抢先置不明确行为于死地。

良定义的终止(不是未定义行为)

若析构函数抛出异常、而此时正因另一个异常进行栈展开,运行时调用 std::terminate(良定义的终止,非未定义行为)。noexcept 函数抛异常、main 抛出未捕获异常、throw 没有匹配的 catch 同样走向 terminate → 默认 std::abort。这正是「别让异常逃离析构函数」的根本原因。

四、实现原理

异常实现原理(知乎)

4.1 异常安全的程序

异常认为在任何位置都会因为编译器的设计而返回。此时需要成对出现的程序就变得有风险。因此需要避免这样的成对关系。

  • 方法的替代
  • fopen,fclose → ofstream fout("out.txt",ios::out); : #include <iostream> #include <fstream>
  • new,delete → std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); : #include <memory>
  • std::mutex mtx; std::lock(mtx); mtx.unlock();: std::lock_guard<std::mutex> lock(mtx); #include <thread> #include <mutex>
  • 线程的join和detach
  • 不使用goto
  • 构造时:面对尚未完全构建好的对象,C++拒绝调用析构函数。故动态内存分配或可能抛出异常的成员对象也要使用智能指针。再次也要在构造函数中添加异常的捕获处理程序。(条款10)
  • 析构时:别让异常逃离析构函数(条款08)(条款11)第一,它可以避免terminate函数在exception传播过程的栈展开机制中被调用(即出现两次没被catch的throw)。第二,它可以协助确保destructors完成其应该完成的所有安排。来源
  • 移动时:移动过程不能有异常。

4.1.1 throw try catch的写法注意

(条款13) throw by pointer: catch方有错处理的风险。 throw by reference: refer to static有多线程问题, refer to non-static 有野指针问题。且复制动作是以静态类型为本,(Widget& rw= localSpecialWidget; throw rw; //抛出Widget,而不是SpecailWidgetthrow by value: 存在一次拷贝(不要期待编译器的优化),

catch by pointer: 返回或抛出指针的一大问题:发送方是好处理了,但接收方若未明确被告知,则不能确定是否要delete。此外与4个标准exceptions 以对象捕获的惯例矛盾。 catch by value: 存在拷贝2次的问题(不要对编译器优化报有期待,除非编译器有明确的catch相关优化选项)。接受动态类型时会出现切割问题(即只捕获到catch声明的静态类型)。 catch by reference: 无截断,无多余拷贝,无delete与否的顾虑。

总结:(条款12)

  • throw测只考虑抛静态对象,需要使用动态编链特性。
  • catch测使用引用接受对象,可以使用动态编链特性。
  • 由于catch到的为时临时变量(理论上),所以无法再catch修改throw的变量。
  • 类型吻合规则:不允许intdouble。允许int*void*
  • first fit策略:为多个catch时,为first fit而非best fit。
  • unexpected throw / more than two throw → terminate → abort
  • exception specifications是把双刃剑。(条款14)C++11 中引入了 noexcept 说明符作为 throw() 的首选替代项。

版本演进(良定义)

动态异常说明 throw(type-list) 在 C++11 起被弃用,C++17 移除throw()(不抛任何异常)作为 noexcept(true) 的弃用等价物保留至 C++20 也被移除。新代码一律用 noexcept。违犯 noexcept(在标记为 noexcept 的函数内抛异常)→ std::terminate(良定义)。

例子:

void test(void){
    throw std::runtime_error("test exception!");//throw、try、catch是关键字
}

int main(){
    try {
        test();
    } catch (std::runtime_error& ex){
    } catch (std::exception& ex){
    } catch(...){}//任意匹配
    return 0;
}

注:隐藏在auto_ptr背后的观念——以一个对象存放“必须自动释放的资源”。

TODO: 锁中抛出异常会怎样? 非局部性数据将导致「内部前后一致性」无法保证,从而不满足基本承诺,即非「异常安全」。

4.1.2 权衡利弊

新式 C++ 中优先使用异常的原因如下:来源

  • 异常会强制调用代码识别并处理错误状态。 未经处理的异常会停止程序执行。
  • 异常跳转到调用堆栈中可以处理错误的位置。 中间函数可以让异常传播。 这些函数不必与其他层协调。
  • 引发异常后,异常堆栈展开机制将根据妥善定义的规则销毁范围内的所有对象。
  • 异常可以在检测错误的代码与处理错误的代码之间实现明确的分离。

4.1.3 三个异常保证

一般来说,异常安全的讨论与一个函数可提供的三个异常保证有关:无故障保证、增强保证和基本保证。

标准术语对照

文中“无故障/增强/基本保证”是机器翻译措辞,对应异常安全理论的标准术语(源自 Sutter / Abrahams,非 ISO C++ 标准定义):no-throw guarantee(不抛出保证)/ strong exception guarantee(强异常安全保证)/ basic exception guarantee(基本异常安全保证)。三者保证强度递减。

无故障保证 无故障(或“无引发”)保证是一个函数可提供的最有力的保证。 此保证声明,该函数将不会引发异常或允许异常传播。 但是,您无法可靠地提供此类保证,除非 (a) 您知道该函数调用的所有函数也是无故障的,或 (b) 您知道将在引发的所有异常到达该函数之前捕获这些异常,或者 (c) 您知道如何捕获和正确地处理可能到达该函数的所有异常。

增强保证和基本保证均依赖析构函数无故障这一假定。 标准库中的所有容器和类型保证其析构函数不会引发。 还有一个相反的要求:标准库要求为其提供的用户定义的类型(例如作为模板参数)必须具有不抛异常的析构函数。

增强保证 增强保证声明,如果函数因异常超出范围,则将不会泄漏内存并且不会修改程序状态。 一个提供增强保证的函数主要是一个提交或回滚语义的事务:它要么完全成功,要么无任何效果。

基本保证 基本保证是三个保证中最弱的一个。 但是,当增强保证对于内存消耗或性能来说很昂贵时,此保证可能是最佳选择。 基本保证声明,如果出现异常,内存不会泄漏并且对象将仍处于可用状态,即使可能已修改数据仍是如此。