异常

条款9:利用destructors避免泄露资源

  • 只要坚持这个规则,把资源封装在对象内(类似智能指针shared_ptr),通常便可以在exceptions出现时避免泄露资源。
  • 简单来说就是,当有资源可能在函数抛异常时而无法释放,这时可以将资源封装到对象内(RAII),利用对象的析构函数来自动释放资源,这样即使有exceptions发生,也不会有资源泄露。

条款10:在constructors内阻止资源泄露(resource leak)

  • C++只会析构已构造完成的对象。对象只有在其constructor执行完毕才算是完成构造妥当。
  • 由于C++不自动清理那些“构造期间抛出exceptions”的对象,所以你必须设计你的constructors,使它们在那种情况下亦能自我清理。通常这只需要将所有可能的exceptions捕捉起来,执行某种清理工作,然后重新抛出exception,使它继续传播出去即可。
  • 但是,更优雅的做法实际上是将这些需要在constructor内初始化的对象视为资源,将它们交由智能指针来管理。
  • 结论是:如果你以auto_ptr(C++11后使用shared_ptr或者unique_ptr)对象来取代pointer class members,你便对你的constructors做了强化工事,免除了“exceptions出现时发生资源泄露”的危机,不再需要在destructors内亲自动手释放资源,并允许const member pointers得以和non-const member pointers有着一样优雅的处理方式。

条款11:禁止异常(exceptions)流出destructors之外

Session::Session()
{
  try {
    logDestruction(this);
  }
  catch (...) {
  }
}
  • 这里的catch语句块看起来什么都没做,但是外表容易骗人。这个语句块阻止了“logDestruction所抛出的exceptions”传出Session destructor之外。
  • 有两个好理由支持我们“全力阻止exceptions传出destructors之外”。第一,它可以避免terminate函数在exception传播过程的栈展开(stack-unwinding)机制中被调用;第二,它可以协助确保destructors完成其应该完成的所有事情。

条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异

  • “抛出exception”与“传递参数”的相同点是:它们的传递方式有3中:by value(传值),by reference(传引用),by pointer(传指针)。然而视你所传递的是参数或exceptions,发生的事情可能完全不同。原因是当你调用一个函数,控制权最终会回到调用端(除非函数失败以至于无法返回),但是当你抛出一个exception,控制权不会再回到抛出端。
  • “抛出exception”与“传递参数”的区别一:C++特别声明,一个对象被抛出作为exception时,总是会发生复制(copy)。即使catch语句参数时by reference,或者抛出对象声明为static也一样会发生复制。且复制动作永远是以对象的静态类型为本。
  • “exception objects必定会造成复制行为”这一事实也解释了“传递参数”和“抛出exception”之间的另一个不同:后者常常比前者慢。
  • 一般而言,你必须使用一下语句:

throw;
才能重新抛出当前的exception,其间没有机会让你改变被传播的exception的类型。此外,它也比较有效率,因为不需要产生新的exception object。

  • “抛出exception”与“传递参数”的区别二:函数调用过程中将一个临时对象传递给一个non-const reference参数时不允许的,但对exception则属合法。
  • “抛出exception”与“传递参数”的区别三:一般而言,调用函数传递参数过程中允许的隐式转换在“exceptions与catch子句相匹配”的过程中使不会发生的。
  • “exceptions与catch子句相匹配”的过程中,仅有两种转换可以发生。第一种是“继承架构中的类转换”。第二种是从一个“有型指针”转为“无型指针”(所以一个参数为const void*的catch子句,可捕捉任何指针类型的exception)。
  • “抛出exception”与“传递参数”的区别四:catch子句总是依出现顺序做匹配尝试。与虚函数调用比较,虚函数采用”best fit“(最佳吻合)策略,而exception处理机制采用”first fit“(最先吻合)策略。因此,绝对不要将”针对base class而设计的catch子句“放在”针对derived class而设计的catch子句“之前。

条款13:以by reference方式捕捉exceptions

  • 相比于by reference方式,以by value方式捕获exception,会使被传递的对象发生两次复制,产生两个副本。其中一个构造动作用于“任何exceptions都会产生的临时对象”身上,另一个构造动作用于“将临时对象复制到catch的参数上”。
  • 千万不要抛出一个指向局部对象的指针,因为该局部对象会在exception传离其scope时被销毁,因此catch子句会获得一个指向“已被销毁的对象”的指针。
  • 如果catch by reference,你就可以避开对象删除问题(by pointer会面对的)——它会让你动辄得咎,做也不是,不做也不是;你也可以避开exception objects的切割问题(派生类对象的exception objects被捕捉并被视为基类对象的exception objects,将失去其派生成分。对象切割问题是由于静态编联导致的,使用引用和指针时不会发生);你可以保留捕捉C++标准exceptions的能力;你也约束了exception objects需被复制的次数。

条款14:明智运用exception specifications

  • exception specification示例,一个只抛出int类型exceptions的函数声明为:
void fun() throw(int);
  • 如果函数抛出一个并未列入exception specification的exception,这个错误会在运行时期被检验出来,于是特殊函数unexpected会被自动调用。unexpected的默认行为是调用terminate。
  • 想要避免这种unexpected的方法就是:

    • 不应该将templates和exception specifications混合使用。
    • 如果A函数内调用了B函数,而B函数无exception specifications,那么A函数本身也不要设定exception specifications。
    • 处理“系统”可能抛出的exceptions(如bad_alloc)。
  • C++允许你以不同类型的exceptions取代非预期的exceptions。如果非预期函数的替代者重新抛出当前的exception,该exception会被标准类型bad_exception取而代之。
void convertUnexpected()
{
  throw;
}

set_unexpected(convertUnexpected);
  • 如果你做了上述安排,并且每一个exception specifications都含有bad_exception,或者其基类,你就再也不必担心程序会于是非预期的exception时中止执行。

了解异常处理(exception handling)的成本

  • 为了让exception的相关成本最小化,只要能够不支持exceptions,编译器便不支持;请将你对try语句块和exception specifications的使用限制于非用不可的地点,并且在真正异常的情况下才抛出exceptions。

吴尼玛
32 声望12 粉丝

记问之学