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

gnuhpc發表於2012-12-04

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

               作者:gnuhpc
               出處:http://www.cnblogs.com/gnuhpc/
               除非另有宣告,本網站採用知識共享“署名 2.5 中國大陸”許可協議授權。


分享到:



相關文章