RAII

RAIIは、日本語では「リソース取得は初期化である」「リソースの確保は初期化時に」「リソースの取得と初期化」などの意味を持ち、資源の確保と解放を、クラス型の変数の初期化と破棄処理に結び付けるというプログラミングのテクニックである。特にC++とD言語で一般的であり、デストラクタをサポートしないC言語などに対する優位性や利便性のうちのひとつとなっている。

RAIIでは、資源の取得をクラス型変数の構築時に、また返却を破壊時に行う。特にプログラムの制御フローが自動変数の属するブロックを抜けるとき、その変数のデストラクタが自動的に呼ばれるため、デストラクタを適切に記述したクラス型変数の寿命が終わるとすぐに資源が返却されることが保証できるようになる。これは例外が発生したときでも同様であるため、RAIIは例外安全なコードを書くための鍵となる概念となった 。

1. 典型的な用法
---------

RAIIの最も基本的な活用例は、動的確保されたメモリを自動解放するスマートポインタ  である。C++においてnew演算子で動的に確保されたメモリは、不要になったときにその領域を指すポインタを経由してdelete演算子で明示的に解放しなければならない。もし解放忘れがあるとメモリリークにつながるが、解放忘れがないように細心の注意を払って、コードパスに手動でひとつひとつ削除処理を記述していくことは非常に手間がかかる。一方、C++ではオブジェクトをスタックに割り当てることも可能であり、動的確保されたメモリを指すポインタをラップするスマートポインタクラスのオブジェクトをスタックに割り当て、ラッパーオブジェクトの寿命が尽きた時点で自動的に呼び出されるデストラクタを利用することにより、動的確保されたメモリの解放を明示的に逐一記述することなく、暗黙的かつ確実に実行させることができる。標準C++ライブラリにおける動的配列クラステンプレートのstd::vectorなども、プログラマが明示的にnew[]およびdelete[]を呼び出す必要のないRAIIクラスの一種である。

RAIIはファイル操作にも用いられる。C言語ではファイルアクセスの際、fopen関数により取得したFILEオブジェクトを明示的にfclose関数で解放することでファイルを閉じる必要があったが、標準C++ライブラリのファイルストリームでは、オブジェクトのコンストラクタでファイルストリームを開き、デストラクタで閉じることで、ファイルハンドルの管理を自動化し、リソースリークを防ぐことができる。このようなファイルアクセスの管理に限らず、C++のデストラクタ機構はあらゆるリソースの寿命管理に活用できる。ほかには、マルチスレッドアプリケーションにおいてクリティカルセクションのロックの管理にもよく用いられる。C++03規格以前においても、Boost C++ライブラリやMicrosoft Foundation Classライブラリなどにクリティカルセクション管理用のRAIIクラスが用意されていたが、C++11規格でスレッドおよび同期オブジェクトが標準化された際に、類似のRAIIクラスがstd::lock_guardおよびstd::unique_lockとして導入された。

また、動的に確保されたメモリの所有権もRAIIで管理できる。所有権が唯一となるスマートポインタクラステンプレートとして、C++03までの標準C++ライブラリではstd::auto_ptrが用意されていたが、C++11以降では非推奨となり、代替のstd::unique_ptrが用意されている。Boost C++ライブラリには類似のクラステンプレートとしてboost::scoped_ptrやboost::interprocess::unique_ptrが実装されている。また、参照カウント方式で所有権を共有するオブジェクトのスマートポインタクラステンプレートとして、Boost C++ライブラリのboost::shared_ptrがある。これはC++11にてstd::shared_ptrとして標準化された。shared_ptrとともに利用する弱参照スマートポインタとして、それぞれboost::weak_ptrおよびstd::weak_ptrが存在する。そのほか、侵入型参照カウント方式のboost::intrusive_ptr、Loki のポリシーベースのLoki::SmartPtr、COMインターフェイスオブジェクト  の参照カウント管理に特化したATLのATL::CComPtrなどがある。

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

2. C++での例
---------

以下、特に断りがない限り、C++03以前でもC++11以降でもコンパイルできるコードで例示する。

2.1. 動的確保されたメモリの管理
------------------

単純な例として、関数内で一時的な作業領域として配列を動的確保することを考える。単純な方法では、以下のようにnew[]演算子を使用する。 void function1A { double* array1 = NULL; double* array2 = NULL; try { // 配列を動的に確保する。メモリ確保失敗により std::bad_alloc 例外がスローされる可能性がある。 array1 = new double[count]; array2 = new double[count];

// 動的に確保した配列をここで作業領域として使用する。 for  { array1[i] = i * 0.1; array2[i] = i * 0.1; } // ...

// 配列を使わなくなったので削除する。 delete[] array2; delete[] array1; } catch  { // 例外がスローされる場合に備える。 delete[] array2; delete[] array1; throw; // 例外の再送出。 } }<WtXmlEndTag /> これはC言語のmallocおよびfree関数による原始的な寿命管理手法に近い。もし動的に確保したメモリを削除する前に関数を抜けるとメモリリークしてしまうため、慎重に削除処理をひとつひとつ記述していく必要がある。動的にメモリ管理するオブジェクトの数が増えるにつれ、ソースコードのメンテナンスコストは増大していく。

一方、RAIIを利用した場合は以下のようになる。 // RAII を実現する配列ラッパークラス。 template class ArrayWrapper { size_t m_count; T* m_data; public: ArrayWrapper : m_count, m_data {} explicit ArrayWrapper : m_count, m_data) {} ~ArrayWrapper { delete[] m_data; } size_t count const { return m_count; } T* data { return m_data; } const T* data const { return m_data; } T& operator[] { return m_data[index]; } const T& operator[] const { return m_data[index]; } // コピーは禁止とする。所有権の移動もサポートしない。 private: ArrayWrapper; ArrayWrapper& operator=; };

void function1B { ArrayWrapper array1; ArrayWrapper array2;

// 動的に確保した配列をここで作業領域として使用する。 for  { array1[i] = i * 0.1; array2[i] = i * 0.1; } // ...

} // RAII 変数 array1, array2 の属するブロックを抜ける。このとき array2, array1 の各デストラクタが順に呼ばれ、それぞれが内部で管理する配列メモリ領域は自動的に破棄される。<WtXmlEndTag />

Boost C++ライブラリのboost::scoped_arrayを使う場合は以下のように書ける。

include <boost/scoped_array.hpp>void function1C { boost::scoped_array array1); boost::scoped_array array2);

for  { array1[i] = i * 0.1; array2[i] = i * 0.1; } }<WtXmlEndTag />

C++11以降のstd::unique_ptrを使う場合は以下のように書ける。

include void function1D { std::unique_ptr<double[]> array1); std::unique_ptr<double[]> array2);

for  { array1[i] = i * 0.1; array2[i] = i * 0.1; } }<WtXmlEndTag />

RAIIを使ってメモリ管理する場合、明示的な削除処理の記述が必要なくなり、コードの見通しやメンテナンス性が向上する。関数の途中でreturnしたり、例外がスローされたりする場合でも、後始末を自動的に実行してくれる。また、C++のテンプレートを利用することで、任意の型に対するRAIIを実現するラッパークラスを定義することができる。C++の標準テンプレートライブラリ  には、RAIIの概念をもとに実装された汎用的な動的配列のクラステンプレートとして、std::vectorが用意されている。

2.2. ファイルハンドルの管理
----------------

別の例として、ファイルのオープンとクローズを挙げる。従来の標準Cライブラリを使って、直接リソースを管理する書き方だと以下のようになる。

include
include
include FILE* openFile { FILE* fp = std::fopen; if  { throw std::runtime_error; } return fp; }

void writeLine { assert; const int ret = std::fprintf; if  { throw std::runtime_error; } }

void function2A { FILE* fp1 = NULL; FILE* fp2 = NULL; try { fp1 = openFile; fp2 = openFile; // ファイルの書き込みを行なう。 writeLine; writeLine; // ファイルを使い続ける。 // 何か問題が起こって関数を抜ける場合、return の前に fclose を忘れずに呼ばなければならない。

// 明示的にファイルを閉じる必要がある。 std::fclose; std::fclose; } catch  { // 獲得したリソースがあれば返却する。 if  { std::fclose; } if  { std::fclose; } throw; // 例外の再送出。 } }<WtXmlEndTag />

一方、RAIIを利用した場合は以下のようになる。 class FileWrapper { FILE* m_fp; public: FileWrapper

m_fp) { // ファイルハンドルでデータメンバーを初期化。if  { throw std::runtime_error; } } ~FileWrapper { assert; std::fclose; } void writeLine { assert; const int ret = std::fprintf; if  { throw std::runtime_error; } } // コピーは禁止とする。所有権の移動もサポートしない。 private: FileWrapper; FileWrapper& operator=; };

void function2B { FileWrapper file1; file1.writeLine; FileWrapper file2; file2.writeLine; } // 関数の途中で return したり、例外がスローされたりしても、RAII 変数の属するブロックを抜けた時点で確実にファイルハンドルは閉じられる。<WtXmlEndTag /> 標準C++ライブラリでは、抽象化されたファイルストリーム管理用のRAIIクラスとして、std::basic_fstreamが用意されている。

FileWrapperクラスではFILE*をカプセル化したが、RAIIの真髄は有限の資源ならば何でも同様に管理できることにある。そしてRAIIでは、関数を抜けるときに適切に資源が破棄されることが保証される。なお、FileWrapperクラスのコンストラクタはファイルが開けなければ例外を投げるため、インスタンスが生成されていれば内包するファイルハンドルは常に利用可能であると仮定してよい。

RAIIを使わない場合、例外が発生するとある問題が生じる。複数の資源を確保する際、それぞれの確保の間に例外が投げられたら、catchブロックではどれを解放すべきか分からなくなってしまう。function1Aやfunction2Aのように、初期値を無効と見なすことにして二段階初期化したり、try-catchブロックを重ねていったりするなど、状況に応じてコードを適切に書いていかなければ安全性は得られない。function1Bやfunction2BのようなRAIIならばこれにも対処できる。変数が構築されたときとは逆順で破棄され、また完全に構築されたオブジェクトのみが破棄されるので問題は起こらない。これはプログラムが資源の管理から逃れることができるようになったということである。RAIIクラスを定義する手間はかかるが、いくつもの関数でRAIIクラスを使っていれば、コードの再利用により全体的にはコードが単純化し、良いプログラムにする手助けとなる。また、副次効果としてビジネスロジックの記述に集中することができるようになる。

なお、function1Aやfunction2AはJavaのようなRAIIでない言語での資源管理に使われるイディオムに似ている。Javaのtry-finallyブロックは資源の確実な返却を実行するポイントを提供するが、都度try-finallyブロックで適切に破棄処理を書かなければならず、プログラマに負担がかかる。

3. 広義のスマートポインタの活用
-----------------

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

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

例えば、stlsoft::scoped_handleは、任意の型の解放関数を受け付け、また0でない広義の「ヌル」値も受け付ける。

以下はWindows APIのファイル入出力関数およびWinsock API関数のリソースをRAIIでラップした例である。CreateFile関数が返す無効値INVALID_HANDLE_VALUEおよびWSASocket関数が返す無効値INVALID_SOCKETは、Windows SDK 8.1ではそれぞれ以下のように定義されている。

define INVALID_HANDLE_VALUE -1))<WtXmlEndTag />

define INVALID_SOCKET <WtXmlEndTag />

RAIIは特に複数のリソースを同時に管理する場合に効果を発揮する。少なくともtry-catch節がいくつも現れて混乱する事態からは逃れられる。

include
include
include
include
include
include
include <stlsoft/smartptr/scoped_handle.hpp>// 3つの資源を同時に使う。 void testScopedHandle { // ファイルを開く。 // CreateFile は失敗した場合 INVALID_HANDLE_VALUE を返す。ただし NULL もまた HANDLE としては一般的に無効値。 HANDLE hFile = ::CreateFileW; if  { throw std::runtime_error; } else { stlsoft::scoped_handle cleanupFile; // ファイルが確実に閉じられるようにする。

// TCP ソケットを作成。 // BSD ソケット API における socket 関数の戻り値は int で、異常値は負数  となっており、0 は正常値のひとつ。 // Winsock もそれを踏襲している。 SOCKET socketDesc = ::WSASocketW; if  { throw std::runtime_error; } else { stlsoft::scoped_handle cleanupSocket; // ソケットが確実に閉じられるようにする。

void *mem = std::malloc; if  { throw std::bad_alloc; } else { stlsoft::scoped_handle<void*> cleanupMem; // メモリが確実に解放されるようにする。

// ここでメモリとソケットとファイルを使う。

const LARGE_INTEGER dummy = {}; if , dummy, NULL, FILE_END)) { throw std::runtime_error; }

const char* text = "Test line.\r "; const DWORD numOfBytesToWrite = static_cast); DWORD numOfBytesWritten = 0; if , text, numOfBytesToWrite, &numOfBytesWritten, NULL) || numOfBytesWritten != numOfBytesToWrite) { throw std::runtime_error; }

// ...

} // mem はここで解放される。 mem = NULL;

// ソケットを自動的な管理から切り離す。 //SOCKET detachedVal = cleanupSocket.detach; //assert;

} // socketDesc を RAII から切り離した場合、ここでは閉じられない。

//const int ecode = ::closesocket; socketDesc = INVALID_SOCKET;

// 早期に hFile の資源を返却することもできる。 //cleanupFile.close;

} // hFile の資源を早期に返却した場合、ここでは返却されない。 hFile = INVALID_HANDLE_VALUE; }

int main { WSADATA wsaData = {}; const int ecode = ::WSAStartup, &wsaData); if  { try { testScopedHandle; } catch  { std::cout << ex.what << std::endl; }

WSACleanup;} }<WtXmlEndTag />

3.1. 制約
-------

RAIIクラスでは解放関数が失敗すると問題になる。C++では言語の制約上デストラクタから例外を投げるのは良い考えではないため、デストラクタではすべての例外を握りつぶす必要がある。エラーコードによる通知も難しくなるため、結果として解放失敗の原因を上位層に通知することが難しくなる。そのためstlsoft::scoped_handleのようなクラスは、次のどちらかに当てはまるときには使うべきではない。

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

4. クロージャとRAII
-------------

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

ファイルの内容を標準出力へprint file.read }

変数'file'はもう存在しない。ファイルハンドルは閉じられた。<WtXmlEndTag />

5. RAIIに類似した制御構造
----------------

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

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

Javaはバージョン7で導入されたtry-with-resources文により、C#のusing文に近い機能を提供する。

6. 脚注
-----

7. 参考文献
-------

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

8. 外部リンク
--------

Article "[1]" by Wikipedia editor Jon Hanna 
Wiki [2]
Sample Chapter "[3]" by Stephen Dewhurst
Article "[4]" by Bill Venners
Article "[5]" by Bjorn Karlsson and Matthew Wilson
[6] by Jonathan Dodds
Article "[7]" by Danny Kalev
Article "[8]" by Mike Nordell
Article "[9]" by Roland Pibinger
Wikibooks RAII in VB6