More C++ Idioms/リソース獲得は初期化である(Resource Acquisition Is Initialization)

出典: フリー教科書『ウィキブックス(Wikibooks)』

リソース獲得は初期化である(Resource Acquisition Is Initialization)
[編集]

意図[編集]

  • スコープの最後でリソースが解放されることを保証する。
  • 基本例外保証を提供する。

別名[編集]

  • オブジェクト生存期間前後での実行(Execute-Around Object)
  • リソース解放は最終化である(Resource Release Is Finalization)

動機[編集]

関数スコープで獲得したリソースは、その所有権が別のスコープやオブジェクトに移動していない限り、そのスコープから離脱する前に解放されるべきである。 ほとんどの場合、これはリソースを獲得する関数とリソースを解放する関数を対にして使うことを意味する。例えば、new/delete, malloc/free, acquire/release, file-open/file-close, nested_count++/nested_count-- (ネスト回数)などである。 リソース管理の「規約」のうち「解放(release)」する部分は非常に容易く書き忘れてしまう。 特に、例外の発生やreturn文によって制御の流れがスコープから離脱することで、リソース解放関数が決して呼び出されないことがある。 現在および未来における全ての可能な場合に対して、プログラマがリソース解放操作を呼び出すと信頼することは非常に危険である。 以下にいくつかの例を示す。

void foo ()
{
  char * ch = new char [100];
  if (...)
     if (...)
        return;
     else if (...)
            if (...)
  else
     throw "ERROR";

  delete [] ch; // 呼び出されないかもしれない……メモリリーク(解放漏れ)!
}
void bar ()
{
  lock.acquire();
  if (...)
     if (...)
        return;
  else
     throw "ERROR";

  lock.release(); // 呼び出されないかもしれない……デッドロック!
}

これは一般的には制御の流れの抽象化の問題である。 リソース獲得は初期化である(Resource Acquisition is Initialization(RAII))イディオムは、C++ では非常に一般的なイディオムであり、賢い方法で「リソース解放」操作の呼び出しの責任を緩和する。

解法とサンプルコード[編集]

発想はリソースの解放操作をそのスコープ中のオブジェクトのデストラクタ中にラップするというものである。 C++規格によって、return 文による場合でも例外による場合でも、制御のフローがスコープを離脱する際に、生成に成功したスタック上のリソース管理オブジェクトのデストラクタが必ず呼び出されることが保証されている。

//  プライベートコピーコンストラクタと代入演算子でNonCopyable派生クラスのコピーを禁止する 
class NonCopyable 
{
   NonCopyable (NonCopyable const &);
   NonCopyable & operator = (NonCopyable const &);
};

class AutoDelete
{
  public:
    AutoDelete (T * p) : ptr_(p) {}
    ~AutoDelete () throw() { delete ptr_; } 
  private:
    T *ptr_;
};
class ScopedLock // スコープ内ロック(Scoped Lock)イディオム
{
  public:
    ScopedLock (Lock & l) : lock_(l) { lock_.acquire(); }
    ~ScopedLock () throw () { lock_.release(); } 
  private:
    Lock lock_;
};
void foo ()
{
  X * p = new X;
  AutoDelete safe_del(p); // メモリはリーク(解放漏れ)しない
  if (...)
    if (...)
      return; 
 
  // ここでは delete を呼ぶ必要はない。
  // safe_del のデストラクタがメモリを delete する。
}
void X::bar()
{
  ScopedLock safe_lock(l); // ロックは確実に解放される
  if (...)
    if (...)
      throw "ERROR"; 
  // ここで release を呼び出す必要はない。
  // safe_lock のデストラクタがロックを解放する。
}

RAII イディオムでは、コンストラクタでのリソース獲得は必須ではないが、デストラクタでのリソース解放が鍵である。 そのため、(稀ではあるが)リソース解放は最終化である(Resource Release is Finalization)イディオムとしても知られている。 このイディオムでは、デストラクタが例外を送出しないことが重要である。 そのため、デストラクタは例外無送出指定(no-throw specification)を持つことがある(必須ではない)。 std::auto_ptr と boost::scoped_ptr によりメモリリソースに対して RAII を素早く使うことができる。 RAII は例外安全性を保証することにも使われる。 RAII は多数の try/catch ブロックの使用なしに、リソースリーク(解放漏れ)を避けることができ、ソフトウェア業界で広く使われている。

RAIIによってリソース管理をしているクラスの中には、正しく必要なコピーをすることのできないものが多い。 (例えばネットワークコネクションやデータベースのカーソル、排他制御)。このNonCopyableクラスはRAIIを実装したオブジェクトのコピーを禁止している。この場合は、単純にコピーコンストラクタとコピー代入をプライベート関数にすることでコピーを妨げている。たとえば、libboostのboost::scoped_ptrはリソースを持っている間はコピーができない。このNonCopyable クラスはこの意図を明示したもので、正しくない場合コンパイルできない。 このようなクラスはSTLコンテナに用いられるべきではない。 しかしながら、RAIIを採用したすべてのリソース管理クラスがコピー不可である必要はない。もし実際にコピーを必要とするのならば、 boost::shared_ptr をメモリリソースの管理に用いることができる。一般的には、コピーの管理には 参照回数計測(Reference Counting) がRAIIと同じように使われる。

帰結[編集]

RAII に制限がないわけではない。 メモリではなく、確定的に解放されなければならず、例外が送出されるかもしれないリソースは、C++ のデストラクタでは大抵うまく扱えない。 C++ のデストラクタでは、(どうやってもそこは終末であり)エラーを外側のスコープに伝えることができないからである。 戻り値はなく、例外を外部に伝播させてはならない。 例外の可能性があるならば、デストラクタはそれ自身の内部でどうにかして例外的な場合を処理しなければならない。 それでもなお、RAII は C++ で最も広く使われているリソース管理イディオムの地位にとどまっている。

既知の利用[編集]

  • ほとんどすべての C++ ソフトウェア
  • std::auto_ptr
  • boost::scoped_ptr
  • boost::mutex::scoped_lock

関連するイディオム[編集]

References[編集]