跳转到内容

更多 C++ 惯用法/资源获取即初始化

来自维基教科书,开放书籍,开放世界

资源获取即初始化

[编辑 | 编辑源代码]
  • 保证在作用域结束时释放资源。
  • 提供基本异常安全保证。

也称为

[编辑 | 编辑源代码]
  • 执行周围对象
  • 资源释放即最终化
  • 作用域绑定资源管理

在函数作用域中获取的资源应在离开作用域之前释放,除非所有权被转移到另一个作用域或对象。这通常意味着一对函数调用 - 一个用于获取资源,另一个用于释放资源。例如,new/delete、malloc/free、acquire/release、file-open/file-close、nested_count++/nested_count-- 等。很容易忘记编写资源管理“契约”的“释放”部分。有时资源释放函数永远不会被调用:当控制流由于返回或异常而离开作用域时,就会发生这种情况。过分相信程序员会在现在和将来在所有可能的情况下调用资源释放操作是极其危险的。下面给出了一些示例。

void foo ()
{
  char * ch = new char [100];
  if (...)
     if (...)
        return;
     else if (...)
            if (...)
  else
     throw "ERROR";

  delete [] ch; // This may not be invoked... memory leak!
}
void bar ()
{
  lock.acquire();
  if (...)
     if (...)
        return;
  else
     throw "ERROR";

  lock.release(); // This may not be invoked... deadlock!
}

这是一个通用的控制流抽象问题。资源获取即初始化 (RAII) 是 C++ 中一个非常流行的惯用法,它以巧妙的方式减轻了调用“资源释放”操作的负担。

解决方案和示例代码

[编辑 | 编辑源代码]

其思想是在作用域中的对象的析构函数中包装资源释放操作。语言保证析构函数(针对成功构造的对象)将在控制流由于 return 语句或异常而离开作用域时始终被调用。

//  Private copy constructor and copy assignment ensure classes derived 
//  from class NonCopyable cannot be copied.
class NonCopyable 
{
   NonCopyable (NonCopyable const &); // private copy constructor
   NonCopyable & operator = (NonCopyable const &); // private assignment operator
};
template <class T>
class AutoDelete : NonCopyable
{
  public:
    AutoDelete (T * p = 0) : ptr_(p) {}
    ~AutoDelete () throw() { delete ptr_; } 
  private:
    T *ptr_;
};

class ScopedLock : NonCopyable// Scoped Lock idiom
{
  public:
    ScopedLock (Lock & l) : lock_(l) { lock_.acquire(); }
    ~ScopedLock () throw () { lock_.release(); } 
  private:
    Lock& lock_;
};

void foo ()
{
  X * p = new X;
  // C++ guarantees that the destructors of objects on the stack will be called, even if an exception is thrown.
  AutoDelete<X> safe_del(p); // destruction of safe_del will be called and lock will be released when it goes out of function foo scope

  if (...)
    if (...)
      return; 
 
  // No need to call delete here.
  // Destructor of safe_del will delete memory
}
void X::bar()
{
  ScopedLock safe_lock(l); // Lock will be released certainly
  if (...)
    if (...)
      throw "ERROR"; 
  // No need to call release here.
  // Destructor of safe_lock will release the lock
}

在 RAII 惯用法中,在构造函数中获取资源不是强制性的,但在析构函数中释放资源是关键。因此,它也被称为(尽管很少)资源释放即最终化惯用法。在此惯用法中,析构函数不应抛出异常这一点很重要。因此,析构函数具有 no-throw 规范,但这是可选的。std::auto_ptr 和 boost::scoped_ptr 是快速将 RAII 惯用法用于内存资源的方法。RAII 还用于确保异常安全。RAII 使得在不广泛使用 try/catch 块的情况下避免资源泄漏成为可能,并且在软件行业中被广泛使用。

许多使用 RAII 管理资源的类没有合法的复制语义(例如,网络连接、数据库游标、互斥锁)。前面显示的NonCopyable 类阻止实现 RAII 的对象的复制。它只是通过将复制构造函数和复制赋值运算符设为私有,来阻止对它们的访问。boost::scoped_ptr 是一个这样的类的示例,它在持有内存资源时阻止复制。NonCopyable 类明确说明了此意图,并在错误使用时阻止编译。此类类不应在 STL 容器中使用。但是,并非每个实现 RAII 的资源管理类都必须像上面这两个示例一样不可复制。如果需要复制语义,则可以使用boost::shared_ptr 来管理内存资源。一般来说,非侵入式引用计数 用于提供复制语义以及 RAII。

RAII 并非没有局限性。不是内存的资源,必须确定性地释放并且可能抛出异常,通常不能很好地由 C++ 析构函数处理。这是因为 C++ 析构函数不能将错误传播到封闭的作用域(这可能会最终导致)。它没有返回值,也不应传播异常。如果可能出现异常,则析构函数必须以某种方式在自身内部处理异常情况。尽管如此,RAII 仍然是 C++ 中使用最广泛的资源管理惯用法。

已知用途

[编辑 | 编辑源代码]
  • 几乎所有非平凡的 C++ 软件
  • std::unique_ptr(和 std::auto_ptr)
  • std::shared_ptr
  • boost::mutex::scoped_lock
[编辑 | 编辑源代码]

参考资料

[编辑 | 编辑源代码]
华夏公益教科书