C++资源管理(RAII)

class ConnectionHandler
{
bool is_conn_close_;
Connection* conn_;
public:

explicit ConnectionHandler(Connection* conn):
conn_(conn),
is_conn_close_(false){}

void Close()
{
try{
conn_ -> close();
}
catch(...){
//...
}
is_conn_close_ = true;
}

operator Connection*()const
{
return conn_;
}

~ConnectionHandler()
{
if(!is_conn_close_){
Close();
}
}

private://nocopyable
ConnectionHandler(const ConnectionHandler&);
ConnectionHandler& operator=(const ConnectionHandler&);
};//class ConnectionHandler

SomeFunction()
{
Driver *driver = get_driver_instance();
ConnectionHandler conn(driver -> connect("..."));
// do something
// maybe throw some exceptions
}

这是一个使用RAII概念来管理资源的示例。这有什么好处呢?
1、自动化。在ConnectionHandler对象超出其作用域,对象销毁时将数据库连接关闭,将不再需要显示的关闭数据库连接操作。自动化的另外一个优点是能杜绝因为粗心等原因导致未释放所申请资源的情况;
2、异常安全。我们来看一下不使用RAII技术时,如何管理数据库连接:
AnotherFunction()
{
Driver *driver = get_driver_instance();
Connection *con = driver -> connect("...");
// do something
// maybe throw some exceptions
conn->close();
}
如果使用这种方法,那么当“do something”时抛出异常,那么“conn->close()”将不会执行,这就导致了数据库连接未能关闭的后果。
而使用RAII技术时,当“do something”时抛出异常,conn对象能够正常的被析构,所以数据库连接仍然能够正常关闭。当然,也可以使用如下方式:
AnotherFunction()
{
Driver *driver = get_driver_instance();
Connection *con = driver -> connect("...");
try{
// do something
// maybe throw some exceptions
}finally{
conn->close();
}
}
来保证数据库连接始终能被关闭。这样做的后果是整个代码看起来较混乱,将正常业务处理逻辑和错误处理逻辑混合在一起。

 

什么是RAII?
RAII(Resource Acquisition is Initialization)的字面意思是资源申请即初始化,即对象构造的时候其所需的资源便应该在构造函数中初始化,而对象析构的时候则释放这些资源[1]。RAII技术依赖与C++98所提供的一种语言机制——对象在超出其作用域时其析构函数将会被自动调用[1]。


智能指针(smart pointer)
C++资源管理中常用的一种工具是智能指针,它实际上就是RAII概念的一种具体实现。C++中智能指针有unique_ptr,shared_ptr,weak_ptr,auto_ptr(因为它的move语义,已不推荐使用)。unique_ptr的简单示例如下:
class Object{//...};
SomeFunciton()
{
std::unique_ptr<Object> up(new Object());
// ...
}
当智能指针对象up离开其作用域时,其所管理的Object对象将会被自动释放,且是异常安全。智能指针的使用有很多需要注意的地方,在这里不细说。

auto_ptr的使用和unique_ptr类似,其简单原型如下:
template<class T>class std::auto_ptr
{
T* ptr_
public:
explicit auto_ptr(T* p=0)throw(){ptr_ = p;}
~auto_ptr(){delete ptr_;}
//...
}
它就是一个RAII概念的范型实现。

ON_SCOPE_EXIT[1]
(这部分直接摘抄于《C++11(及现代C++风格)和快速迭代式开发》——BY 刘未鹏 )
不过,RAII范式虽然很好,但还不足够易用,很多时候我们并不想为了一个CloseHandle, ReleaseDC, GlobalUnlock等等而去大张旗鼓地另写一个类出来,所以这些时候我们往往会因为怕麻烦而直接手动去调这些释放函数,手动调的一个坏处是,如果在资源申请和释放之间发生了异常,那么释放将不会发生,此外,手动释放需要在函数的所有出口处都去调释放函数,万一某天有人修改了代码,加了一处return,而在return之前忘了调释放函数,资源就泄露了。理想情况下我们希望语言能够支持这样的范式:

void foo()
{
HANDLE h = CreateFile(...);

ON_SCOPE_EXIT { CloseHandle(h); }

... // use the file
}
ON_SCOPE_EXIT里面的代码就像是在析构函数里面的一样:不管当前作用域以什么方式退出,都必然会被执行。

实际上,早在2000年,Andrei Alexandrescu 就在DDJ杂志上发表了一篇文章,提出了这个叫做ScopeGuard 的设施,不过当时C++还没有太好的语言机制来支持这个设施,所以Andrei动用了你所能想到的各种奇技淫巧硬是造了一个出来,后来Boost也加入了ScopeExit库,不过这些都是建立在C++98不完备的语言机制的情况下,所以其实现非常不必要的繁琐和不完美,实在是戴着脚镣跳舞(这也是C++98的通用库被诟病的一个重要原因),再后来Andrei不能忍了就把这个设施内置到了D语言当中,成了D语言特性的一部分(最出彩的部分之一)。

再后来就是C++11的发布了,C++11发布之后,很多人都开始重新实现这个对于异常安全来说极其重要的设施,不过绝大多数人的实现受到了2000年Andrei的原始文章的影响,多多少少还是有不必要的复杂性,而实际上,将C++11的Lambda Function和tr1::function结合起来,这个设施可以简化到脑残的地步:

class ScopeGuard
{
public:
explicit ScopeGuard(std::function<void()> onExitScope)
: onExitScope_(onExitScope), dismissed_(false)
{ }

~ScopeGuard()
{
if(!dismissed_)
{
onExitScope_();
}
}

void Dismiss()
{
dismissed_ = true;
}

private:
std::function<void()> onExitScope_;
bool dismissed_;

private: // noncopyable
ScopeGuard(ScopeGuard const&);
ScopeGuard& operator=(ScopeGuard const&);
};
这个类的使用很简单,你交给它一个std::function,它负责在析构的时候执行,绝大多数时候这个function就是lambda,例如:

HANDLE h = CreateFile(...);
ScopeGuard onExit([&] { CloseHandle(h); });
onExit在析构的时候会忠实地执行CloseHandle。为了避免给这个对象起名的麻烦(如果有多个变量,起名就麻烦大了),可以定义一个宏,把行号混入变量名当中,这样每次定义的ScopeGuard对象都是唯一命名的。

#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)
#define ON_SCOPE_EXIT(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)

Dismiss()函数也是Andrei的原始设计的一部分,其作用是为了支持rollback模式,例如:

ScopeGuard onFailureRollback([&] { /* rollback */ });
... // do something that could fail
onFailureRollback.Dismiss();

在上面的代码中,“do something”的过程中只要任何地方抛出了异常,rollback逻辑都会被执行。如果“do something”成功了,onFailureRollback.Dismiss()会被调用,设置dismissed_为true,阻止rollback逻辑的执行。

ScopeGuard是资源自动释放,以及在代码出错的情况下rollback的不可或缺的设施,C++98由于没有lambda和tr1::function的支持,ScopeGuard不但实现复杂,而且用起来非常麻烦,陷阱也很多,而C++11之后立即变得极其简单,从而真正变成了每天要用到的设施了。C++的RAII范式被认为是资源确定性释放的最佳范式(C#的using关键字在嵌套资源申请释放的情况下会层层缩进,相当的不能scale),而有了ON_SCOPE_EXIT之后,在C++里面申请释放资源就变得非常方便

Acquire Resource1
ON_SCOPE_EXIT( [&] { /* Release Resource1 */ })

Acquire Resource2
ON_SCOPE_EXIT( [&] { /* Release Resource2 */ })

这样做的好处不仅是代码不会出现无谓的缩进,而且资源申请和释放的代码在视觉上紧邻彼此,永远不会忘记。更不用说只需要在一个地方写释放的代码,下文无论发生什么错误,导致该作用域退出我们都不用担心资源不会被释放掉了。我相信这一范式很快就会成为所有C++代码分配和释放资源的标准方式,因为这是C++十年来的演化所积淀下来的真正好的部分之一。[1]

[1]http://mindhacks.cn/2012/08/27/modern-cpp-practices/

posted @ 2013-05-18 11:40  Apprentice_  阅读(535)  评论(1编辑  收藏  举报