【C++设计技巧】C++中的RAII机制

转载 2015年11月21日 13:47:22

作者:gnuhpc 
出处:http://www.cnblogs.com/gnuhpc/

1.概念

Resource Acquisition Is Initialization 机制是Bjarne Stroustrup首先提出的。要解决的是这样一个问题:

在C++中,如果在这个程序段结束时需要完成一些资源释放工作,那么正常情况下自然是没有什么问题,但是当一个异常抛出时,释放资源的语句就不会被执行。于是Bjarne Stroustrup就想到确保能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。将初始化和资源释放都移动到一个包装类中的好处:

  • 保证了资源的正常释放
  • 省去了在异常处理中冗长而重复甚至有些还不一定执行到的清理逻辑,进而确保了代码的异常安全。
  • 简化代码体积。

2.应用场景

1)文件操作

我们可以是用这个机制将文件操作包装起来完成一个异常安全的文件类。实现上,注意将复制构造函数和赋值符私有化,这个是通过一个私有继承类完成的,因为这两个操作在此并没有意义,当然这并不是RAII所要求的。

/*
 * =====================================================================================
 *
 *       Filename:  file.cpp
 *
 *    Description:  RAII for files
 *
 *        Version:  1.0
 *        Created:  05/09/2011 06:57:43 PM
 *       Revision:  none
 *       Compiler:  g++
 *
 *         Author:  gnuhpc, warmbupt@gmail.com
 *
 * =====================================================================================
 */
#include <IOSTREAM>
#include <STDEXCEPT>
#include <CSTDIO>

using namespace std;
class NonCopyable
{
public:
NonCopyable(){};
private:
    NonCopyable (NonCopyable const &); // private copy constructor
    NonCopyable & operator = (NonCopyable const &); // private assignment operator
};

class SafeFile:NonCopyable{
public:
    SafeFile(const char* filename):fileHandler(fopen(filename,"w+"))
    {
        if( fileHandler == NULL )
        {
            throw runtime_error("Open Error!");
        }
    }
    ~SafeFile()
    {
        fclose(fileHandler);
    }

    void write(const char* str)
    {
        if( fputs(str,fileHandler)==EOF )
        {
            throw runtime_error("Write Error!");
        }
    }

    void write(const char* buffer, size_t num)
    {
        if( num!=0 && fwrite(buffer,num,1,fileHandler)==0 )
        {
            throw runtime_error("Write Error!");
        }
    }
private:
    FILE *fileHandler;
    SafeFile(const SafeFile&);
    SafeFile &operator =(const SafeFile&);
};

int main(int argc, char *argv[])
{
    SafeFile testVar("foo.test");
    testVar.write("Hello RAII");
}

C++的结构决定了其原生支持RAII,而在Java 中,对象何时销毁是未知的,所以在Java 中可以使用try-finally做相关处理。

2)智能指针模拟

一个更复杂一点的例子是模拟智能指针,抽象出来的RAII类中实现了一个操作符*,直接返回存入的指针:

现在我们有一个类:

class Example {
  SomeResource* p_;
  SomeResource* p2_;
public:
  Example() :
    p_(new SomeResource()),
    p2_(new SomeResource()) {
    std::cout << "Creating Example, allocating SomeResource!/n";
  }

  Example(const Example& other) :
    p_(new SomeResource(*other.p_)),
    p2_(new SomeResource(*other.p2_)) {}

  Example& operator=(const Example& other) {
    // Self assignment?
    if (this==&other)
      return *this;

    *p_=*other.p_;
    *p2_=*other.p2_;
    return *this;
  }

  ~Example() {
     std::cout << "Deleting Example, freeing SomeResource!/n";
     delete p_;
     delete p2_;
  }
};

假设在创建SomeResource的时候可能会有异常,那么当p_指向的资源被创建但p2_指向的资源创建失败时,Example的实例就整个创建失败,那么p_指向的资源就存在内存泄露问题。

用下边的这个方法可以为权宜之计:

Example() : p_(0),p2_(0)
{
  try {
    p_=new SomeResource();
    p2_=new SomeResource("H",true);
    std::cout << "Creating Example, allocating SomeResource!/n";
  }
  catch(...) {
    delete p2_;
    delete p_;
    throw;
  }
}

但是我们可以利用一个对象在离开一个域中会调用析构函数的特性,在构造函数中完成初始化,在析构函数中完成清理工作,将需要操作和保护的指针作为成员变量放入RAII中。

template <TYPENAME T>
class RAII {
  T* p_;
public:
  explicit RAII(T* p) : p_(p) {}

  ~RAII() {
    delete p_;
  }

  void reset(T* p) {
    delete p_;
    p_=p;
  }

  T* get() const {
     return p_;
  }

  T& operator*() const {
     return *p_;
  }

  void swap(RAII& other) {
    std::swap(p_,other.p_);
  }

private:
  RAII(const RAII& other);
  RAII& operator=(const RAII& other);
};

我们在具体使用把保护的指针Someresource放在RAII中:

class Example {
  RAII<SOMERESOURCE> p_;
  RAII<SOMERESOURCE> p2_;
public:
  Example() :
    p_(new SomeResource()),
    p2_(new SomeResource()) {}

  Example(const Example& other)
    : p_(new SomeResource(*other.p_)),
      p2_(new SomeResource(*other.p2_)) {}

  Example& operator=(const Example& other) {
    // Self assignment?
    if (this==&other)
      return *this;

    *p_=*other.p_;
    *p2_=*other.p2_;
    return *this;
  }

  ~Example() {
    std::cout << "Deleting Example, freeing SomeResource!/n";
  }
};

现在即使p_成功而p2_失败,那么在Stack winding时也会调用RAII的析构函数保证了p_指向的Someresource被析构。这种方法较之例1中需要实现被组合的指针类型相应的接口不同,这里不需要对接口进行封装。当然,在例1中,你也可以提供一个getPointer的函数直接将句柄提供出来。

其实在Example中,已经不需要析构函数了,因为RAII类会帮它照顾好这一切的。这有点像auto_ptr,本文并不打算深入讨论智能指针这个话题。

3)锁操作

/*
 * =====================================================================================
 *
 *       Filename:  threadlock.cpp
 *
 *    Description:  Lock for RAII
 *
 *        Version:  1.0
 *        Created:  05/09/2011 10:16:13 PM
 *       Revision:  none
 *       Compiler:  g++
 *
 *         Author:  gnuhpc (http://blog.csdn.net/gnuhpc), warmbupt@gmail.com
 *
 * =====================================================================================
 */
#include <CSTDIO>
#include <STDLIB.H>
#include <PTHREAD.H>

int counter = 0;
void* routine(void *ptr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

class NonCopyable
{
public:
    NonCopyable(){};
private:
    NonCopyable (NonCopyable const &); // private copy constructor
    NonCopyable & operator = (NonCopyable const &); // private assignment operator
};

class ScopeMutex:NonCopyable
{
public:
    ScopeMutex(pthread_mutex_t* mutex):mutex_(mutex){
        pthread_mutex_lock( mutex_ );
    }

    ~ScopeMutex(){
        pthread_mutex_unlock( mutex_ );
    }
private:
    pthread_mutex_t *mutex_;
};

int main(int argc, char *argv[])
{
    int rc1, rc2;
    pthread_t thread1, thread2;
    if( (rc1=pthread_create( &thread1, NULL, routine, NULL)) )
    {
        printf("Thread creation failed: %d/n", rc1);
    }

    if( (rc2=pthread_create( &thread2, NULL, routine, NULL)) )
    {
        printf("Thread creation failed: %d/n", rc1);
    }
    pthread_join( thread1, NULL);
    pthread_join( thread2, NULL);
}

void* routine(void *ptr)
{
    ScopeMutex scopeMutex(&mutex);
    counter++;
    printf("%d/n",counter);
}

3.总结

RAII机制保证了异常安全,并且也为程序员在编写动态分配内存的程序时提供了安全保证。缺点是有些操作可能会抛出异常,如果放在析构函数中进行则不能将错误传递出去,那么此时析构函数就必须自己处理异常。这在某些时候是很繁琐的。

4.参考文献

http://www.codeproject.com/KB/cpp/RAIIFactory.aspx  这篇文章用工厂方法的方式完成了一个RAII工厂。

http://www.informit.com/articles/printerfriendly.aspx?p=21084 讨论了异常安全的一些情况,其中提到赋值符的安全值得注意。

 

作者:gnuhpc 
出处:http://www.cnblogs.com/gnuhpc/

C++中的RAII机制详解

前言 在写C++设计模式——单例模式的时候,在写到实例销毁时,设计的GC类是很巧妙的,而这一巧妙的设计就是根据当对象的生命周期结束时会自动调用其析构函数的,而这一巧妙的设计也是有专业的名词的——...

C++中的RAII机制

前言在写C++设计模式——单例模式的时候,在写到实例销毁时,设计的GC类是很巧妙的,而这一巧妙的设计就是根据当对象的生命周期结束时会自动调用其析构函数的,而这一巧妙的设计也是有专业的名词的——RAII...

C++之RAII机制[转]

原文:C++中的RAII机制什么是RAII?RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++...

boost学习之RAII机制(资源申请即初始化)

/* 先介绍下一个专业词汇:RAII(Resource Application Immediately Initialize)资源申请即初始化。 这也是智能指针的基本原理,智能指针只是这个习语的其中...

【C++程序设计技巧】Pimpl机制

作者:gnuhpc  出处:http://www.cnblogs.com/gnuhpc/ 1.简介 这个机制是Private Implementation的缩写,我们常常听到诸如“不要改动你的公...
  • sesiria
  • sesiria
  • 2016年06月01日 14:39
  • 226

RAII惯用法:C++资源管理的利器

RAII是指C++语言中的一个惯用法(idiom),它是“Resource Acquisition Is Initialization”的首字母缩写。中文可将其翻译为“资源获取就是初始化”。虽然从某种...

C++中利用析构避免资源泄露(RAII技术)

C++中利用析构避免资源泄露(RAII技术)

RAII惯用法:C++资源管理的利器

RAII是指C++语言中的一个惯用法(idiom),它是“Resource Acquisition Is Initialization”的首字母缩写。中文可将其翻译为“资源获取就是初始化”。虽然从某种...

RAII惯用法:C++资源管理的利器

cocos2dx window

effective c++条款13-17 “以对象管理资源”之RAII浅析

RAII是指C++语言中的一个惯用法(idiom),它是“Resource Acquisition Is Initialization”的首字母缩写。中文可将其翻译为“资源获取就是初始化”。虽然从某种...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:【C++设计技巧】C++中的RAII机制
举报原因:
原因补充:

(最多只允许输入30个字)