好学IT学院:IT信息技术分享交流平台
标签:C++  来源:互联网  作者:非典型秃子  发布时间:2009-05-17  ★★★加入收藏〗〖手机版
摘要:关于C++中异常的争论何其多也,但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性,幸运的是,随着C++社区经验的积累,今天我们已经有足够的知识轻松编写异常安全的代码了,而且编写异常安全的代码一般也不会对性能造成影响。…

然后,代码只需这样写:

scoped_lock guard(a_lock);
do_something...

清晰而优雅!继续考察这个例子,假设我们并不需要成对操作, 显然,修改scoped_lock构造函数即可解决问题。然而,往往方法名称和参数也不是那么固定的,怎么办?可以借助这样一个辅助类:

template<typename FEnd, typename FBegin>
struct pair_guard{
  pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}
   ~pair_guard(){m_fe();}
private:
   FEnd m_fe;
   ...//禁止复制
};
typedef pair_guard<function<void () > , function<void()> > simple_pair_guard;

好了,借助boost库,我们可以这样来编写代码了:

simple_pair_guard guard(bind(&Lock::unlock, a_lock), bind(&Lock::lock, a_lock) );
do_something...

我承认,这样的代码不如前面的简洁和容易理解,但是它更灵活,无论函数名称是什么,都可以拿来结对。我们可以加强对bind的运用,结合占位符和reference_wrapper,就可以处理函数参数、动态绑定变量。所有我们在catch内外的相同工作,交给pair_guard去完成即可。

考察前面的几个例子,也许你已经发现了,所谓异常安全的代码,竟然就是如何避免try...catch的代码,这和直觉似乎是违背的。有些时候,事情就是如此违背直觉。异常是无处不在的,当你不需要关心异常或者无法处理异常的时候,就应该避免捕获异常。除非你打算捕获所有异常,否则,请务必把未处理的异常再次抛出。try...catch的方式固然能够写出异常安全的代码,但是那样的代码无论是清晰性和效率都是难以忍受的,而这正是很多人抨击C++异常的理由。在C++的世界,就应该按照C++的法则来行事。

如果按照上述的原则行事,能够实现基本保证了吗?诚恳地说,基础设施有了,但技巧上还不够,让我们继续分析不够的部分。

对于一个方法常规的执行过程,我们在方法内部可能需要多次修改对象状态,在方法执行的中途,对象是可能处于非法状态的(非法状态 != 未知状态),如果此时发生异常,对象将变得无效。利用前述的手段,在pair_guard的析构中修复对象是可行的,但缺乏效率,代码将变得复杂。最好的办法是......是避免这么作,这么说有点不厚道,但并非毫无道理。当对象处于非法状态时,意味着此时此刻对象不能安全重入、不能共享。现实一点的做法是:

a.每一次修改对象,都确保对象处于合法状态
  b.或者当对象处于非法状态时,所有操作决不会失败。

在接下来的强保证的讨论中细述如何做到这两点。

强保证是事务性的,这个事务性和数据库的事务性有区别,也有共通性。实现强保证的原则做法是:在可能失败的过程中计算出对象的目标状态,但是不修改对象,在决不失败的过程中,把对象替换到目标状态。考察一个不安全的字符串赋值方法:

string& operator=(const string& rsh){
  if (this != &rsh){
      myalloc locked_pool(m_data);
      locked_pool.deallocate(m_data);
      if (rsh.empty())
      m_data = NULL;
      else{
      m_data = locked_pool.allocate(rsh.size() + 1);
      never_failed_copy(m_data, rsh.m_data, rsh.size() + 1);
      }
  }
  return *this;
}

locked_pool是为了锁定内存页。为了讨论的简单起见,我们假设只有locked_pool构造函数和allocate是可能抛出异常的,那么这段代码连基本保证也没有做到。若allocate失败,则m_data取值将是非法的。参考上面的b条目,我们可以这样修改代码:

myalloc locked_pool(m_data);
  locked_pool.deallocate(m_data);   //进入非法状态
  m_data = NULL;          //立刻再次回到合法状态,且不会失败
  if(!rsh.empty()){
  m_data = locked_pool.allocate(rsh.size() + 1);
  never_failed_memcopy(m_data, rsh.m_data, rsh.size() + 1);
}

现在,如果locked_pool失败,对象不发生改变。如果allocate失败,对象是一个空字符串,这既不是初始状态,也不是我们预期的目标状态,但它是一个合法状态。我们阐明了实现基本保证所需要的技巧部分,结合前述的基础设施(RAII的运用),完全可以实现基本保证了...哦,其实还是有一点疏漏,不过,那就留到最后吧。

继续,让上面的代码实现强保证:

myalloc locked_pool(m_data);
  char* tmp = NULL;
  if(!rsh.empty()){
  tmp = locked_pool.allocate(rsh.size() + 1);
  never_failed_memcopy(tmp, rsh.m_data, rsh.size() + 1); //先生成目标状态
  }
  swap(tmp, m_data);     //对象安全进入目标状态
  m_alloc.deallocate(tmp);  //释放原有资源

  • 好学触屏公众号虎力全开、杨帆起航!
  • 四大名著全套小学生版注音版
  • 诗词中的科学全4册
  • 曹文轩系列儿童文学全套画本