僵尸对象或 RAII

我最近在想这个问题,到底要不要在程序中使用异常?

以前写的 C 代码比较多,即使写 C++,基本上也是把它当成 C with object 来用。对异常的了解偏少,使用更是极少。最近评审别人代码的时候遇到一个问题:如果构造函数中 new 失败了,会发生什么事情?

工程的代码一般提倡哪里出错在哪里处理,不能恢复的要返回错误码给调用者。在一般情况下,使用 new(std::no_throw) 保证 new 不抛出异常(否则结果是灾难性的),并且检查分配是否成功是可以实现这一点的。

遗憾的是构造函数没有返回值,我们不能返回构造失败。那么只有用迂回的办法,为类定义一个成员变量 bool inited。初始化为 false,只有在构造的工作都完成之后,才将它置为 true。如果一个对象的 inited 成员为 false,就意味着它构造过程中出了问题,不能被使用。这就是一个僵尸对象,“活死人”。

看,我们成功地规避了使用异常。但是慢着,不是只有 bad_alloc 这一个异常啊!还有 bad_cast、runtime_error、logic_error,还有:

$ grep class /usr/include/c++/4.5/stdexcept 
// Standard exception classes  -*- C++ -*-
// ISO C++ 19.1  Exception classes
   *  program runs (e.g., violations of class invariants).
   *  @brief One of two subclasses of exception.
  class logic_error : public exception 
  class domain_error : public logic_error 
  class invalid_argument : public logic_error 
  class length_error : public logic_error 
  class out_of_range : public logic_error 
   *  @brief One of two subclasses of exception.
  class runtime_error : public exception 
  class range_error : public runtime_error 
  class overflow_error : public runtime_error 
  class underflow_error : public runtime_error 

天那,我未曾注意过标准库有那么多异常!那么如果在使用标准库时,不小心触发了什么异常,OMG!

这样看来,使用异常是很有必要的。但是,麻烦的问题又来了,一旦使用异常,函数的退出过程就变了。使用错误码有一个好处,就是你可以在函数返回前擦干净自己的屁股;但是使用异常呢?你既要保证对象能够自己擦屁股(RAII),还要保证函数能自己擦屁股(在正确的位置使用异常处理),这样才能在 stack unwinding 时不会导致内存泄露。哦,auto_ptr 可以帮上一些忙,但如果是分配的资源是数组呢?

还有一个麻烦是,你要遵从约定——特别是对于一个程序库作者来说。如果约定出错时抛出异常,那么可以抛;如果约定出错时返回错误码,或者这个库可能被 C 调用,那么抛出异常就可能是灾难。

现在看来,如果想实现更健壮的 C++ 程序,那么异常处理是不可或缺的。但在使用异常处理之前,必须得了解在哪里、怎样抛出和捕获异常,如果是团队合作,可能还需要有简单的操作指导手册,否则使用不当或者过量的异常也可能带来麻烦。

我还在路上!

《僵尸对象或 RAII》上有7条评论

  1. 这个问题在google c++style guideC++ FAQ给出了解决方法:只使用构造器作为memset一类的初始化工作,而使用返回自身指针(及错误status)的init函数进行“逻辑上的”初始化。

    实际上,objective-c一直以来都是这种范式,如[NSTimer initWithFireDate: interval: target: selector: userInfo: repeats:]这样的函数即可用NSTimer *timer=[[NSTimer alloc] initWithFireDate:...];这样的方法来初始化。

  2. 敝司C++ sytle guilde中明确禁止用异常,模板等很fancy的东西,也几乎从不检查memory分配失败的情况,俺刚来时很不习惯,被告知“memory都分配失败了,你的程序异常也没什么大不了的了”。嗯,估计在EDA行业,大家对软件crash的容忍阈值都比较高吧,君不见那S家的VCS照样经常crash的一塌糊涂么。。。

  3. 绝对不要尝试在C++里面玩异常,无数的人有无数血的教训。简单的做法是:抛了异常就exit吧。然后回头检查如何避免抛异常。内存不足了,就要避免内存不足。new都不能new了,你这个系统还怎么运行?这不搞笑吗?

    1. 试图在new失败之后还继续运行的人,不知道oom_killer吧?这种事情需要从头避免,而不是躲闪回避。

      1. 呵呵,估计你是写客户端程序较多,而不是服务端程序。OOM不一定非得退出,对于服务器来说,可能稍等一会儿就能分配到内存。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注