RAII

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動: 案内検索

RAIIResource Acquisition Is Initialization、日本語では「リソースの確保は初期化時に」、「リソースの取得と初期化」など)は、資源(リソース)の確保と解放を変数の初期化と破棄処理に結び付けるというプログラミングのテクニックで、特にC++Dで一般的である。

RAIIでは資源の取得を変数の構築(初期化)時、返却を破壊時に行う。自動変数がスコープを離れるときデストラクタが呼ばれるため、変数の寿命が終わるとすぐに資源が返却されることが保障できるようになった。これは例外が起こったときでも同様であるため、RAIIは例外安全なコードを書くための鍵となる概念となった (Sutter 1999)。

典型的な用法[編集]

RAIIはよくマルチスレッドアプリケーションにおいてスレッドのロックの管理に用いられる。ほかにはファイル操作にも用いられ、標準C++ライブラリファイルストリームでは、オブジェクトのコンストラクタでファイルストリームを開き、デストラクタで閉じる。C++ではオブジェクトをスタックに割り当てることが可能で、このようにC++のスコープの機構はファイルアクセスの管理に活用できる。

後の例のようにRAIIは例外安全の達成にも活用される。RAIIを使えばあちこちにtry/catchブロックを用いることなくリソースリークを防げる。

動的に確保されたメモリの所有権もRAIIで管理できる。この用途に標準C++ライブラリではunique_ptrが用意されている。さらに、共有されるオブジェクトの生存期間は、C++の場合Boostのboost::shared_ptr(C++11:std::shared_ptr)・参照カウント方式、boost::intrusive_ptr・侵入型参照カウント方式、LokiポリシーベースのLoki::SmartPtrによって所有権を共有するという管理の仕方ができる。

C++での例[編集]

例外を送出しない関数OpenFileとCloseFileが与えられたとする。次のようにログファイルを開いたとする。

class LogFile
{
public:
    class FileFailedToOpen {};
    class WriteFailure {};
    LogFile(const char* fileName)
      : file( OpenFile(fileName) ) { // ファイルハンドルでデータメンバを初期化
        if (file == INVALID_HANDLE)
            throw FileFailedToOpen(); // エラーが起きたら
    }
 
    ~LogFile() {
        CloseFile(file);
    }
 
    void write(const char* logLine) {
        WriteFile(file, logLine);
        if (error)
            throw WriteFailure();
    }
 
private:
    FILE_HANDLE file;
 
    // コピーを防ぐためprivateに宣言のみ書いておく
    LogFile (const LogFile&);
    LogFile& operator= (const LogFile&);
};

このRAIIに則ったクラスは次のように使用できる。

void functionA()
{
    LogFile log("Output log of functionA");
 
    log.write("fun Step succeeded");
 
    // ... logを使い続ける ...
    // 例外が送出されようと、関数からreturnが行われようと確実にlogのファイルハンドルは閉じられる。
}

RAIIを使わない場合、ログ出力する関数は手動でファイルを管理しなければならない。この例は先と等価に書いたものである。

void functionB()
{
    FILE_HANDLE log = OpenFile("Output log of functionB");
 
    // 追加コード: 明示的なエラーの確認
    if (log == INVALID_HANDLE)
        throw FileFailedToOpen();
 
    try {
        WriteFile(log, "fun Step succeeded");
 
        // エラーを確認する必要がある場合
        if (error)
             throw WriteFailure();
         // ... logを使い続ける ... 
         // 何か問題が起こって関数を抜ける場合、returnの前にCloseFile(log)を忘れずに呼ばなければならない。
 
        // 追加コード: 明示的に閉じなければならない
        CloseFile(log);
    } catch (...) {
        // この関数や更に内から例外が投げられた
 
        // 追加コード: 獲得したリソースを返却する
        CloseFile(log);
 
        throw; // 再送出
  }
}

LogFileとfunctionBの実装はOpenFileやCloseFileが例外を投げる可能性があった場合更に複雑になるが、functionAはそのような心配が不要である。

LogFileではFILE_HANDLEをカプセル化したが、RAIIの真髄は有限の資源なら何でも同様に管理できることにある。そして関数(やその他ブロック)を抜けるときに適切に資源が破棄されることがRAIIでは保障される。更にLogFileのインスタンスは(ファイルが開けなければ例外を投げるため)常に利用可能であると仮定してよい。

RAIIを使わない場合、例外が発生するとある問題が生じる。functionBで、複数の資源が確保されているとして、それぞれの確保の間に例外が投げられたら、catchブロックではどれを解放すべきかわからないのである(通常、確保されていない資源を解放することはできない)。RAIIならこれにも対処できる。変数(メンバ変数も含む)が構築されたときとは逆順で破棄され、また完全に構築された(内部で例外が投げられずにコンストラクタが実行された)オブジェクトのみが破棄されるので問題は起こらない。functionBは初期値を無効と見なすことにしたりtry-catchブロックを重ねていったりするなど、状況に応じてコードを適切に書いていかなければfunctionAのような安全さは得られないのである。

これはfunctionAが資源(またはそれに類するもの)の管理から逃れることができるようになったということである。いくつもの関数でLogFileを使っていれば、全体的にはコードが単純化し、コードの再利用もなっており、良いプログラムにする手助けとなっている。

functionBはJavaのようなRAIIでない言語での資源管理に使われるイディオムに似ている。Javaのtry-finallyブロックは資源の確実な返却を促すが、例えば全ての関数がLogFileを使うとしたら、その度try-finallyブロックで適切に破棄処理を書かなければならず、プログラマに負担がかかる。

スマートポインタへの適用[編集]

訳注: この節では広い意味でスマートポインタという言葉を使っている。一般的にはメモリに特化したものをスマートポインタと言う。

RAIIをスマートポインタと呼ばれる類のクラスを使い、任意のリソース管理APIへ適用することも可能である。functionBを簡略化すると次のようになる。

void functionC()
{
    FILE_HANDLE log = OpenFile("Output log of functionC");
 
    // 追加コード: 明示的なエラーの確認
    if (log == INVALID_HANDLE)
        throw FileFailedToOpen();
 
    stlsoft::scoped_handle<FILE_HANDLE> cleanup(log, CloseFile, INVALID_HANDLE); // logはこれで「安全」になった。もう資源の漏れはない。
 
    WriteFile(log, "fun Step succeeded");
} // CloseFile()はスコープが外れるここで自動的に呼ばれる。

このコンポーネントは、(voidを含む)任意の型の解放関数を受け付け、0でない「ヌル」(無効)値(例えばINVALID_HANDLEが(INVALID_HANDLE)~0である場合など)を受け付ける(Windowsのように複数の呼出規約が用いられる環境では、どんなものでも受け付ける)。

複数のインスタンスを同時に使っても問題ない。上の例と比べればcatch節がいくつも現れて混乱する事態からは逃れられる。

void functionD() // 3つの資源を同時に使う
{
    FILE_HANDLE fh = OpenFile("some-file");
 
    if (fh != INVALID_HANDLE) {
        stlsoft::scoped_handle<FILE_HANDLE> cleanup(log, CloseFile, INVALID_HANDLE); // ファイルハンドルを「安全」にする
 
        SOCKET_HANDLE sk = OpenSocket("service-host");
 
        if (sk != INVALID_SOCKET_HANDLE) {
            stlsoft::scoped_handle<SOCKET_HANDLE> cleanup2(sk, CloseSocket, INVALID_SOCKET_HANDLE); // ソケットを「安全」にする
 
            void *mem = malloc(10000);
 
            if(NULL != mem) {
                stlsoft::scoped_handle<void*> cleanup3(mem, free); // メモリが確実に解放されるようにする
 
                // ここでメモリとソケットとファイルハンドルを使う。
 
            } // 'mem'はここで解放される。
 
            // ソケットを自動的な管理から切り離す。
            SOCKET_HANDLE sk2 = cleanup2.detach();
 
        } // 'sk'はcleanup2から切り離されたので、ここでは閉じられない。
 
        cleanup.close(); // 早期に'fh'の資源を返却する。
 
    } // 'fh'の資源は既に返却されているので、ここでは返却されない。
}

最初のLogFileのようなRAIIクラスでは解放関数が失敗すると問題になる。C++では言語の制約上デストラクタから例外を投げるのは良い考えではない。そのためscoped_handleのようなクラスの利用は次のどちらかに当てはまるときには使うべきではない。

  1. 解放関数が失敗する可能性のある場合
  2. 利用者がその失敗を知るべき場合

クロージャとRAII[編集]

RubySmalltalkは特別なスコープに関連付けられた変数の中にあるクロージャブロックという形でRAIIに対応している。以下はRubyの例である。

File.open("data.txt") { |file|
    # ファイルの内容を標準出力へ
    print file.read
}
# 変数'file'はもう存在しない。ファイルハンドルは閉じられた。

RAIIに類似した制御構造[編集]

C#VB .NET 2005はC++デストラクタに代わるSystem.IDisposableインターフェイスを実装するクラスとusing文を使ってRAIIに似た機能を実現している。

Python 2.5に追加された「with」ステートメントでは、同様の目的に__entry__と__exit__のメソッドを使う。

参考文献[編集]

  • Sutter, Herb (1999). Exceptional C++. Addison-Wesley. ISBN 978-0-201-61562-3.
    • 日本語訳 ハーブ・サッター 『Exceptional C++』 浜田真理、ピアソンエデュケーション、2000年、249頁。ISBN 978-4-89471-270-6

外部リンク[編集]