C++中RAII的惯用方法

时间:2023-01-31 20:56:49

软件开发中,会用到各种各样的资源。狭义的资源指内存,而广义的资源包括文件、网络连接、数据库连接、信号量、事件、线程、内存等,甚至可以是状态。资源获取后由于种种原因导致永久不能释放的资源称为资源泄漏。针对资源泄漏,提出了各种各样的软件机制和程序设计惯用法,如垃圾收集、RRID[1]、RAII、确定性资源清理等。

RAII是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

本文简单介绍RAII的分类以及如何使用RAII,以使代码安全地管理资源。

RAII的分类

根据RAII对资源的所有权可分为常性类型和变性类型,代表者分别是boost:shared_ptr<>[2]和std::auto_ptr<>;从所管资源的初始化位置上可分为外部初始化类型和内部初始化类型。

常性类型是指获取资源的地点是构造函数,释放点是析构函数,并且在这两点之间的一段时间里,任何对该RAII类型实例的操纵都不应该从它手里夺走资源的所有权。变性类型是指可以中途被设置为接管另一个资源,或者干脆被置为不拥有任何资源。外部初始化类型是指资源在外部被创建,并被传给RAII实例的构造函数,后者进而接管了其所有权。boost:shared_ptr<>和std::auto_ptr<>都是此类型。与之相对的是内部初始化类型。

其中,常性且内部初始化的类型是最为纯粹的RAII形式,最容易理解,最容易编码。


最后,不得不提醒RAII的理念固然简单,不过在具体实现的时候仍有需要小心的地方。比如对于STL的auto_ptr,可以视为资源的代理对象,auto_ptr对象间的赋值是一个需要特别注意的地方。简单说来资源代理对象间赋值的语义不满足“赋值相等”,其语义是资源管理权的转移。

什么是“赋值相等”呢?比如:

int a;  int b = 10;  a = b; //这句话执行后 a == b 但对于资源代理对象,这是不满足的,比如:

auto_ptr
<int> a(null);  auto_ptr<int> b(new int(123));  a = b; //这句话执行后a != b,赋值的语义是b把资源的管理权交给了a 

auto_ptr是这样一种指针:它是“它所指向的对象”的拥有者。这种拥有具有唯一性,即一个对象只能有一个拥有者,严禁一物二主。当auto_ptr指针被摧毁时,它所指向的对象也将被隐式销毁,即使程序中有异常发生,auto_ptr所指向的对象也将被销毁。


关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr
<int> p(new int(42));     //OK
std::auto_ptr<int> p = new int(42);    //ERROR
这是因为auto_ptr 的构造函数被定义为了explicit
5、不要把auto_ptr放入容器

RAII实际应用

每当处理需要配对的获取/释放函数调用的资源时,都应该将资源封装在一个对象中,实现自动资源释放。例如,我们无需直接调用一对非成员函数OpenPort/ClosePort,而是可以考虑定义常性且内部初始化的RAII概念的“端口”操作类:

 
  1. class Port{  
  2. public:  
  3. Port(const string& destination);//调用OpenPort  
  4. ~Port();//调用ClosePort  
  5. };  
  6. void DoSomething(){  
  7. Port port1(“server1:80”);  
  8. …  
  9. }  
  10. shared_ptr<Port> post2 = /*…*///port2在最后一个引用它的  
  11. //shared_ptr离开作用域后关闭 

通过使用上述RAII类型,可以避免程序员忘记关闭端口而引起的泄漏,还可以确保异常发生时栈展开过程中自动释放端口资源。

RAII与STL容器

STL容器是基于值语义的,在容器内部,对象是常被复制的。如果RAII类型需要存入STL容器,需要作一些处理。

 
  1. class Resource   
  2. {   
  3. public:   
  4. Resource() {/*分配资源*/}   
  5. ~ Resource() {/*释放资源*/}   
  6. private:   
  7. int handle;   
  8. };   
  9. std::map< Identifier, Resource > resourceMap;  

以上代码中STL容器对Resource的复制将导致运行期错误。最好的方法是让RAII类型继承于boost::noncopyable[2],而后在容器中使用引用计数的指针:

 
  1. class Resource : public boost::noncopyable   
  2. {   
  3. public:   
  4. Resource() {/*分配资源*/}   
  5. ~ Resource() {/*释放资源*/}   
  6. private:   
  7. int handle;   
  8. };   
  9. typedef boost::shared_ptr<Resource> PointerToResourceType;   
  10. typedef std::map< Identifier, PointerToResourceType> ResourceMapType;   
  11. ResourceMapType resourceMap;  

作为替代,还可以使用非拷贝行为的容器:boost::ptr_map<Identifier,Resource> map;

域守卫类

广义的资源可代表状态。这时,域守卫类(scoping classes)所带来的安全价值是无法衡量的。例如:对于在多线程应用中用于同步线程的Mutex,ScopedLock类用于实现锁/解锁的操作:

 
  1. class ScopedLock {  
  2. public:  
  3. explicit ScopedLock (Mutex& m) : mutex(m) { mutex.lock(); locked = true; }  
  4. ~ScopedLock () { if (locked) mutex.unlock(); }  
  5. void unlock() { locked = false; mutex.unlock(); }  
  6. private:  
  7. ScopedLock (const ScopedLock&);  
  8. ScopedLock& operator= (const ScopedLock&);  
  9. Mutex& mutex;  
  10. bool locked;  
  11. }; 

当ScopedLock实例对象被创建时,mutex就被锁定了,而当实例作用域生命期结束时mutex隐式释放。通过这种方法避免了忘记释放的锁,从而避免了此原因所引起的死锁和崩溃。

 
  1. {  
  2. ScopedLock locker(mtx);  
  3. …  
  4. // 自动释放 

ps:这个锁的使用方法,和Muduo中的使用方法一样。其实RAII是一种设计理念,并非

为每一种资源建立一个RAII类型会使代码显得冗长且容易出错。使用ScopeGuard模板类能够写出简单、异常安全和避免资源泄漏的代码。

 
  1. {  
  2. void *buffer = std::malloc(1024);  
  3. ScopeGuard freeIt = MakeGuard(std::free, buffer);  
  4. FILE *fp = std::fopen("afile.txt");  
  5. ScopeGuard closeIt = MakeGuard(std::fclose, fp);  
  6. …  

总结

RAII的核心思想是使用对象管理资源,对象“消亡”则自动释放资源。理解和使用RAII能使软件设计更清晰,代码更健壮。与大名鼎鼎的垃圾收集(GC)不同的是,RAII可管理广义的资源,而垃圾收集只关注“内存泄漏”,不关心诸如文件句柄、同步对象等一些系统资源的泄漏问题。RAII能使程序员确定资源释放的时机,这也正是C++/CLI引入确定性资源清理的原因。

RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。由此可见,RAII惯用法是进行资源管理的有力武器。C++程序员依靠RAII写出的代码不仅简洁优雅,而且做到了异常安全