RAII手法

如何确保资源一定会被释放(即便发生异常),这在D里面对应的是scope(exit),在Java里面对应的是finally,在C#   里面对应的是scoped using。

简而言之就是,不管当前作用域以何种方式退出,某某操作(通常是资源释放)都一定要被执行。

这个问题的答案其实C++程序员们应该耳熟能详了:RAII。RAII是C++最为强大的特性之一。在C++里面,局部变量的析构函数刚好满足这个语意:无论当前作用域以何种方式退出,所有局部变量的析构函数都必然会被倒着调用一遍。所以只要将有待释放的资源包装在析构函数里面,就能够保证它们即便在异常发生的情况下也会被释放掉了。为此C++提供了一系列的智能指针:auto_ptr、scoped_ptr、scoped_array… 此外所有的STL容器也都是RAII的。在C++里面模拟D的scope(exit)也是利用的RAII。

RAII相较于java的finally的好处和C#的scoped using的好处是非常明显的。只要一段代码就高下立判:

  // in Java
  String ReadFirstLineFromFile( String path )
  {
    StreamReader r = null;
    String s = null;
    try {
      r = new StreamReader(path);
      s = r.ReadLine();
    } finally {
      if ( r != null ) r.Dispose();
    }
    return s;
  }

  // in C#
  String ReadFirstLineFromFile( String path ) 
  {
    using ( StreamReader r = new StreamReader(path) ) {
      return r.ReadLine();
    }
  }

显然,Java版本的(try-finally)最臃肿。C#版本(scoped using)稍微好一些,但using毕竟也不属于程序员关心的代码逻辑,仍然属于代码噪音;况且如果不连续地申请N个资源的话,使用using就会造成层层嵌套结构。 

如果使用RAII手法来封装StreamReader类的话(std::fstream就是RAII类的一个范例),代码就简化为: 

  // in C++, using RAII
  String ReadFirstLineFromFile(String path)
  {
    StreamReader r(path);
    return r.ReadLine();
  }

 好处是显而易见的。完全不用担心资源的释放问题,代码也变得“as simple as possible”。此外,值得注意的是,以上代码只是演示了最简单的情况,其中需要释放的资源只有一个。其实这个例子并不能最明显地展现出RAII强大的地方,当需要释放的资源有多个的时候,RAII的真正强大之处才被展现出来,一般地说,如果一个函数依次申请N个资源:

  void f()
  {
    acquire resource1;
    … 
    acquire resource2;
    … 
    acquire resourceN;
    … 
  }

 那么,使用RAII的话,代码就像上面这样简单。无论何时退出当前作用域,所有已经构造初始化了的资源都会被析构函数自动释放掉。然而如果使用try-finally的话,f()就变成了:

  void f()
  {
    try {
      acquire resource1;
      … // #1
      acquire resource2;
      … // #2
      acquire resourceN;
      … // #N
    } finally {
      if(resource1 is acquired) release resource1;
      if(resource2 is acquired) release resource2;
      …
      if(resourceN is acquired) release resourceN;
    }
  }

     为什么会这么麻烦呢,本质上就是因为当执行流因异常跳到finally块中时,你并不知道执行流是从#1处、#2处…还是#N处跳过来的,所以你不知道应该释放哪些资源,只能挨个检查各个资源是否已经被申请了,如果已申请了便将其释放;要能够检查每个资源是否已经被申请了,往往就要求你要在函数一开始将各个资源的句柄全都初始化为null,这样才可以通过if(hResN==null)来检查第N个资源是否已经申请。

      最后,RAII其实是scope(exit)的特殊形式。但在资源释放方面,RAII有其特有的优势:如果使用scope(exit)的话,每个资源分配之后都需要用一个scope(exit)跟在后面保护起来;而如果用RAII的话,一个资源申请就对应于一个RAII对象的构造,释放工作则被隐藏在对象的析构函数中,从而使代码主干保持了清爽。

你可能感兴趣的:(C++基础)