《More Effective C++ 》读书笔记(二)Exception 异常

这事篇读书笔记,只记录自己的理解和总结,一般情况不对其举例子具体说明,因为那正是书本身做的事情,我的笔记作为梳理和复习之用,划重点。我推荐学C++的人都好好读一遍Effective C++ 系列,真是好书啊,对于学完C++ 基础知识的人,这是本高阶秘籍。

笔记

条款 9 - 15 关注的主题是异常。关注1、异常可能引起的资源泄露(强烈推荐使用智能指针)2、异常是如何抛出的和3、异常的成本。目的是写出 exception-safe 的程序。

  • 条款9: 利用 destructor避免资源泄露
    这个条款是说如果异常被抛出到调用端,但是调用端没有catch, 如果调用端是指针,用来析构指针的delete语句可能因为异常被抛出而跳过,导致资源泄露。最好的办法就是不用指针,用临时对象,或者说用行为类似指针的临时对象(为什么非要用类似指针的行为呢?为了多态),临时对象会在对象生存期结束的时候析构,在析构函数中delete(如果有指针的话) 释放资源。这里的原则就是所谓RAII(Resource Acquisition Is Initialization),在具体实践上就是使用智能指针,C++11 中提供了std::shared_ptr 和 std::unique_ptr,目的就是为了避免资源泄露。

  • 条款10:在 constructor 内阻止资源泄露。 这个条款举例子说明了如果在对象构造时发生了异常,这个时候因为构造未完成故不会调用该对象的析构函数,该对象的指针成员面临着没有delete 的风险,导致资源泄露。解决办法跟条款9一样,想用指针的时候使用智能指针。

  • 条款11:禁止异常流出destructors 之外。 这个条款是要说明,如果异常发生在destuctor中,不要将其抛出。 理由1是因为如果这个destuctor是因为某个异常而调用的,在这时候再次发生异常并抛出会导致程序 terminate,程序将会终结,也就是最不喜欢看到的程序崩了。处理方式是把异常给吞了,如下面的例子。理由2是如果异常被抛出,引起异常的函数后面的代码都没有被执行,析构函数没有析构完。因此要尽力阻止异常被destrucor 抛出。

~AClass(){
    try{
        a_function_may_throw_exception();
    }catch(...){
        // done nothing.
    }
}
  • 条款12:了解“抛出一个异常excption” 与“传递一个参数”或者“调用一个虚函数”之间的差异。 首先得明确一点, 抛出异常,事实上也是抛出异常对象,看上去跟传递对象参数是很类似的。(1) 但是,异常在抛出的时候总是伴随着对象copy行为(想想这是为什么,提示与局部对象生存期有关),也因此在效率上发生折扣。常见参数的传递的方式有 by value, by reference, by pointer. 不管是 by value 还是 by reference 抛出异常实例都会引起复制,捕捉异常时 by value 还要多复制一次。 throw by pointer 和 pass by pointer。 都是传递指针的副本,千万不要传递一个指向局部对象的指针,否则会获得一个指向已销毁的对象的指针。(2) 传递exception 不会发生隐式类型转化,除非是有型指针转为 void* 或者是继承架构中的转化(catch base 的可以catch derived 的异常),catch 语句中遵循 first fit, 即第一个匹配的就被捕捉,所以 base exception 会拦截 derived excetion。(因此应该让catch derived excaption 的语句放在前面)。
  • 条款13:以 by reference 的方式捕捉异常。 无论如何不要用 by pointer 方式捕捉异常; by value 传递会有slice 问题而且比 by reference 多复制一次。综合条款 12 和13 ,结论就是使用 by reference 的方式捕捉异常。
  • 条款14:明智运用exception specifications。 这是一把双刃剑,一方面对于函数希望提供什么样的exception 提供了说明,另外也带来了不安全的行为。unexcepted 函数默认终结程序,如果违反了 exception specification 会调用unexception函数。记住1.不要和模板 template 混用;2.不要对回调函数使用exception specification.

Note:

  1. void foo() throw(); // 含义是不抛出异常
  2. void foo() throw(A,B,C); // 含义是可能抛出A,B,C 型异常
  3. 书中76 页中介绍了 set_unexpected()函数和将非预期的异常转化为已知异常的技术,可以较为安全的处理unexpected 发生。
  • 条款15:了解异常处理(excption handling)的成本。 成本主要是三点:1.为了支持异常,程序需要做大量簿记工作,消耗额外的内存和时间,而且因为只要程序库或者用户代码任何一处用了异常处理,编译器就必须提供对异常处理的支持能力。 2. 编译器实现try语句块和 excpetion specification 带来的代码膨胀和性能损失,2000年之前作者写这本书的时候,得到的消息和测试结果是 5%-10%的损失,不知道现在谁否有提升。 3.抛出异常的损失,可能比正常的函数返回要慢三个数量级。注意,这个只是在发生异常并抛出的时候才有,而异常应该是比较少发生才是。

总结

  1. 为了异常安全,使用智能指针;
  2. 析构函数不要抛出异常,应该吞掉;
  3. 捕捉catch 异常应该 by reference;
  4. 异常处理是有成本的。(迷思:这些成本如今是否得到优化,或者项目可以容忍?如果我开始一个新项目,我要不要使用异常处理?)
posted @ 2018-11-12 00:02  行者孙  阅读(358)  评论(0编辑  收藏  举报