【C++設計技巧】C++中的RAII機制


作者: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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM