RAII
RAII(Resource Acquisition Is Initialization)は、日本語では「リソース取得は初期化である」「リソースの確保は初期化時に」「リソースの取得と初期化」などの意味を持ち、資源(リソース)の初期化(確保)と後始末(解放)を、クラス型の変数の初期化と後始末に結び付けるというプログラミングのテクニックである。特にC++とD言語で一般的であり、デストラクタをサポートしないC言語などに対する優位性や利便性のうちのひとつとなっている。
RAIIでは、資源の取得をクラス型変数の構築(初期化)時に、また返却を破壊時に行う。特にプログラムの制御フローが自動変数の属するブロックを抜けるとき、その変数のデストラクタが自動的に呼ばれるため、デストラクタを適切に記述したクラス型変数の寿命が終わるとすぐに資源が返却されることが保証できるようになる。これは例外が発生したときでも同様であるため、RAIIは例外安全なコードを書くための鍵となる概念となった (Sutter 1999)。
RAIIの最も基本的な活用例は、動的確保されたメモリを自動解放するスマートポインタ (smart pointer) である。C++においてnew演算子(またはnew[]演算子)で動的に確保されたメモリは、不要になったときにその領域を指すポインタを経由してdelete演算子(または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 (C++)のポリシーベースのLoki::SmartPtr、COMインターフェイスオブジェクト (IUnknown) の参照カウント管理に特化したATLのATL::CComPtrなどがある。
後の例のようにRAIIは例外安全の達成にも活用される。RAIIを使えばあちこちにtry-catchブロックを記述することなくメモリリークやリソースリークを防げる。
以下、特に断りがない限り、C++03以前でもC++11以降でもコンパイルできるコードで例示する。
単純な例として、関数内で一時的な作業領域として配列を動的確保することを考える。単純な方法では、以下のようにnew[]演算子を使用する。
これはC言語のmallocおよびfree関数による原始的な寿命管理手法に近い。もし動的に確保したメモリを削除する前に関数を抜けるとメモリリークしてしまうため、慎重に削除処理をひとつひとつ記述していく必要がある。動的にメモリ管理するオブジェクトの数が増えるにつれ、ソースコードのメンテナンスコストは増大していく。
一方、RAIIを利用した場合は以下のようになる。
Boost C++ライブラリのboost::scoped_arrayを使う場合は以下のように書ける。
C++11以降のstd::unique_ptrを使う場合は以下のように書ける。
RAIIを使ってメモリ管理する場合、明示的な削除処理の記述が必要なくなり、コードの見通しやメンテナンス性が向上する。関数の途中でreturnしたり、例外がスローされたりする場合でも、後始末を自動的に実行してくれる。また、C++のテンプレートを利用することで、任意の型に対するRAIIを実現するラッパークラスを定義することができる。C++の標準テンプレートライブラリ (STL) には、RAIIの概念をもとに実装された汎用的な動的配列のクラステンプレートとして、std::vectorが用意されている。
別の例として、ファイルのオープンとクローズを挙げる。従来の標準Cライブラリを使って、直接リソースを管理する書き方だと以下のようになる。
一方、RAIIを利用した場合は以下のようになる。
標準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ブロックで適切に破棄処理を書かなければならず、プログラマに負担がかかる。
スマートポインタと呼ばれる類のクラスを使い、RAIIを任意のリソース管理APIへ適用することも可能である。
なお、この節では広い意味でスマートポインタという言葉を使っている。一般的にはメモリに特化したものをスマートポインタと言う。
例えば、stlsoft::scoped_handleは、(voidを含む)任意の型の解放関数を受け付け、また0でない広義の「ヌル」値(無効値)も受け付ける(Windowsのように複数の呼出規約が用いられる環境では、どんなものでも受け付ける)。
以下はWindows APIのファイル入出力関数およびWinsock API関数のリソースをRAIIでラップした例である。CreateFile()関数が返す無効値INVALID_HANDLE_VALUEおよびWSASocket()関数が返す無効値INVALID_SOCKETは、Windows SDK 8.1ではそれぞれ以下のように定義されている。
RAIIは特に複数のリソースを同時に管理する場合に効果を発揮する。少なくともtry-catch節がいくつも現れて混乱する事態からは逃れられる。
RAIIクラスでは解放関数が失敗すると問題になる。C++では言語の制約上デストラクタから例外を投げるのは良い考えではないため、デストラクタではすべての例外を握りつぶす必要がある。エラーコードによる通知も難しくなるため、結果として解放失敗の原因を上位層に通知することが難しくなる。そのためstlsoft::scoped_handleのようなクラスは、次のどちらかに当てはまるときには使うべきではない。
RubyとSmalltalkは特別なスコープに関連付けられた変数の中にあるクロージャブロックという形でRAIIに対応している。以下はRubyの例である。
C#とVB .NET 2005はC++デストラクタに代わるSystem.IDisposableインターフェイスを実装するクラスとusing文を使ってRAIIに似た機能を実現している。
Python 2.5に追加されたwithステートメントでは、同様の目的に__enter__と__exit__のメソッドを使う。
Javaはバージョン7で導入されたtry-with-resources文により、C#のusing文に近い機能を提供する。