一种优雅的资源管理技术——RAII

时间:2022-10-11 23:58:51

      RAII技术,很有意思,与其说是一个技术,不如说是一个编程上的窍门。我们平常编程时也可能会用到,在stl模板库里也有体现,但可能我们并不知道它的名字。

      RAII,即Resource acquisition is initialization”,也就是“资源获取就是初始化”。恩,就是这样。啥意思?不知道(个人感觉这个名字确实取得不怎么样)。

      好,让我们抛开名字,直接用它的简写RAII。RAII简单的说,是为了防止诸如内存泄露、资源泄露等情况产生的一种编程技巧。我们平常在编程时,经常会遇到申请堆内存、获取windows系统资源handle的情况。而这些情况下,均需要我们手动的显示释放你之前申请的内存或资源。我们可以理解为有“借”必须有“还”(在运用锁的情况下,可以看做有“关”必须有“开”,不然就会死锁)。

     但是,你能够保证做到你编写的代码有“借”了,一定就会“还”吗?比如我们获取了windows文件资源的句柄,接下来我们执行了若干操作,中间可能有某些条件下的return语句,甚至还会抛出异常,你能足够细心在每个return及异常的catch中释放句柄资源吗?好,即使我们都释放了,那么代码中会有多处重复的释放资源代码,总是不够那么优雅。

     不用慌张,RAII能让我们优雅的做完释放资源这件事。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终一定会被调用。我们前面说过,“借”了,一定要“还”。注意到那两个红色的“一定”了吗?一个是C++中一定会调用的函数,一个是我们一定要做的事。那我们把一定要做的事情放到一定会调用的函数中可以吗?这样我们就保证了一定要做的事最终一定会发生!

     没错,RAII就是利用析构函数一定会被调用的特性(不管是在return还是在异常退出的情况下),将释放资源的代码写到类的析构函数中。在类对象初始化的时候,将资源传入该类对象中“托管”,并利用类对象最终调用析构函数将托管的资源释放掉。

     这样,资源的生命周期就等同于类对象的生命周期。

     我们提炼出RAII的三个关键词:

  • 构造函数 (将资源传入类对象,实现“托管”)
  • 生命周期(在类对象的生命周期内,托管的资源不会释放)
  • 析构函数(当类对象生命周期结束(或异常退出时,在进入catch语句前,会自动调用对象析构函数),调用其析构函数,托管的资源在析构函数中同时也释放掉)

显然,要实现RAII,我们需要设计一个符合上述关键词的类,而对于这种类的对象,我们称之为RAII对象(RAII Object)
那么就上面管理文件资源句柄的例子而言,我们可以设计下面一个RAII类。
// HandleMgr.h
class CHandleMgr
{
public:
CHandleMgr(const HANDLE& khandle);
virtual ~CHandleMgr();
void ReleaseHandle();
HANDLE& GetHandle() { return m_handle; }
private:
HANDLE m_handle;
bool m_bReleased;
};

// HandleMgr.cpp
CHandleMgr::CHandleMgr(const HANDLE& khandle) : m_handle(khandle), m_bReleased(false)
{}
CHandleMgr::ReleaseHandle()
{
if (m_handle != nullptr)
{
CloseHandle(m_handle);
m_handle = nullptr;
}
m_bReleased = true;
}
CHandleMgr::~CHandleMgr
{
try
{	if (!m_bReleased)	{		if (m_handle != nullptr)        	{			CloseHandle(m_handle);			m_handle = nullptr;        	}	}
}
catch(...) { // do sty }
}

  注意,1、在CHandleMgr类中,我添加了一个ReleaseHandle方法,这样,我们通过显示调用 ReleaseHandle来释放资源,不必非要等到CHandleMgr类生命周期结束。
2、在CHandleMgr析构函数中,有一个try/catch语句,它会捕获所有的C++异常。放在这里的作用是“决不让异常跑出析构函数”(《Effective C++》)。由于对于资源的释放,有时候是危险的,在某些情况下会抛出异常。而让异常逃出析构函数会导致对象析构不完整,同时,请注意,由于C++中try语句只能够捕获一个异常,那么下面的代码片段
CTestClass 
{
public:
….
~CTestClass()
{
….
//may throw exception 2
}
…..
};

int main()
{
try
{
……….
CTestClass a;
………
// may throw exception 1
}catch(…)
{
// do sht
}
}
    在main函数的代码中,有可能会抛出异常1。好,当异常1抛出后,被main中的try捕获到,在进入catch前,调用CTestClass的析构函数,但不幸的是,这时候析构函数也抛出了异常2,由于这里只有一个try,并且被异常1用掉了,那么异常2则不会被捕获,导致程序的崩溃。
上面大致是对RAII的描述。其实ReleaseHandle方法并不是必须的,我们可以完全利用对象的生命周期来释放资源。如果要灵活的控制对象的生命周期,我们只需在适当的地方加上{}即可。当程序执行过{},那么{}里面的对象则超出了作用域,生命周期结束,资源释放。
RAII在锁的应用中也是常见的,比如有时候我加了锁,但却忘记解锁,导致死锁。而在C++ 11 中的lock_guard和unique_lock中,则利用了RAII来实现了对锁的自动解锁。下面是stl中lock_guard的实现,在其类内部实现了对锁的管理,在其析构函数中,释放了锁资源。
template <class _Mutex>
class _LIBCPP_TYPE_VIS lock_guard
{
public:
typedef _Mutex mutex_type;

private:
mutex_type& __m_;
public:

_LIBCPP_INLINE_VISIBILITY
explicit lock_guard(mutex_type& __m) // 构造函数,接收锁资源,并自动上锁
: __m_(__m) {__m_.lock();}
_LIBCPP_INLINE_VISIBILITY
lock_guard(mutex_type& __m, adopt_lock_t)
: __m_(__m) {}
_LIBCPP_INLINE_VISIBILITY
~lock_guard() {__m_.unlock();} // 析构函数, 释放锁资源

private:
lock_guard(lock_guard const&);// = delete;
lock_guard& operator=(lock_guard const&);// = delete;
};
     而在std::ofstream中,则实现了对文件handle的自动管理,类似我上面的代码,有兴趣的话大家可以看一下stl源码是怎样实现的。