リソース取得は初期化である( RAII ) [ 1 ]は、いくつかのオブジェクト指向の静的型付けプログラミング言語で特定の言語動作を記述するために使用されるプログラミングイディオム[ 2 ]です。RAII では、リソースを保持することはクラス不変条件であり、オブジェクトのライフタイムに結びついています。リソースの割り当て(または取得) は、オブジェクトの作成時 (具体的には初期化時) にコンストラクタによって行われ、リソースの解放 (解放) は、オブジェクトの破棄時 (具体的にはファイナライゼーション時) にデストラクタによって行われます。言い換えれば、初期化が成功するためには、リソースの取得が成功する必要があります。したがって、初期化が終了してからファイナライゼーションが開始されるまでの間、リソースが保持されることが保証され (リソースを保持することはクラス不変条件です)、オブジェクトが生きている間だけ保持されます。したがって、オブジェクトリークがなければ、リソースリークもありません。
RAII は、その起源であるC++と最も強く関連付けられていますが、Ada [ 3 ] 、Vala [ 4 ]、Rust [ 5 ]とも関連付けられています。この技術は、1984~1989 年にかけて、主にBjarne StroustrupとAndrew Koenig [ 7 ]によってC++ [ 6 ]で例外安全なリソース管理のために開発され、この用語自体は Stroustrup によって造語されました[ 8 ] 。
このイディオムの他の名称としては、コンストラクタ取得、デストラクタ解放(CADRe) [ 9 ]があり、特定の使用スタイルはスコープベースのリソース管理(SBRM) [ 10 ]と呼ばれています。後者の用語は、自動変数の特殊なケースに使用されます。RAII はリソースをオブジェクトのライフタイムに結び付けますが、これはスコープの開始と終了とは一致しない場合があります。(特に、フリー ストアに割り当てられた変数のライフタイムは、特定のスコープとは無関係です。) ただし、自動変数 (SBRM) に RAII を使用することが最も一般的な使用例です。
以下の例は、ファイルアクセスとミューテックスロックにおけるRAIIの使用方法を示しています。
import std ;using std :: mutex ; using std :: ofstream ; using std :: runtime_error ; using std :: scoped_lock ; using std :: string ;void writeToFile ( const string & message ) { // mutex はファイルへのアクセスを保護するためのものです (スレッド間で共有されます)。static mutex m ;// ファイルにアクセスする前にミューテックスをロックします。scoped_lock < mutex > lock ( m );// ファイルを開こうとする。ofstream f { "example.txt" }; if ( ! f . is_open ()) { throw runtime_error ( "ファイルを開けません" ); }// メッセージをファイルに書き込む。std :: println ( f , message );// スコープを抜けるとき、まずファイルが閉じられます(例外の有無に関わらず)。// スコープを抜けるとき、次にミューテックスのロックが解除されます(ロックデストラクタから)。 // (例外の有無に関わらず)。}このコードは例外安全です。なぜなら、C++ では自動記憶域期間を持つすべてのオブジェクト (ローカル変数) が、囲んでいるスコープの終了時に構築の逆順で破棄されることが保証されているからです。[ 11 ]そのため、例外がスローされたかどうかに関わらず、関数から戻るときにロックオブジェクトとファイルオブジェクト の両方のデストラクタが呼び出されることが保証されます。[ 12 ]
ローカル変数を使用すると、単一の関数内で複数のリソースを簡単に管理できます。ローカル変数は構築された順序とは逆の順序で破棄され、オブジェクトは完全に構築された場合にのみ破棄されます。つまり、コンストラクタから例外が伝播しない場合に破棄されます。[ 13 ]
RAII を使用すると、リソース管理が大幅に簡素化され、コード全体のサイズが削減され、プログラムの正確性が確保されます。そのため、RAII は業界標準のガイドラインで推奨されており、[ 14 ] C++ 標準ライブラリのほとんどがこの慣習に従っています。[ 15 ]
RAIIをリソース管理手法として用いる利点は、カプセル化、例外安全性(スタックリソースの場合)、および局所性(取得ロジックと解放ロジックを隣接して記述できる)を提供することである。
リソース管理ロジックがクラス内で一度だけ定義され、呼び出し箇所ごとに定義されないため、カプセル化が実現されます。スタックリソース(取得時と同じスコープ内で解放されるリソース)の例外安全性は、リソースをスタック変数(特定のスコープ内で宣言されたローカル変数)の有効期間に紐付けることで確保されます。例外がスローされ、適切な例外処理が実装されている場合、現在のスコープを抜ける際に実行されるコードは、そのスコープ内で宣言されたオブジェクトのデストラクタのみです。最後に、クラス定義内でコンストラクタとデストラクタの定義を隣り合わせに記述することで、定義の局所性が確保されます。
したがって、リソース管理は、適切なオブジェクトのライフサイクルと連動させることで、自動的な割り当てと解放を実現する必要があります。リソースは初期化時に取得されるため、利用可能になる前に使用される可能性はなく、同じオブジェクトの破棄によって解放されます。この破棄は、エラーが発生した場合でも確実に実行されます。
RAII をfinallyJava で使用される構造と比較し、Stroustrup は次のように書いています。「現実的なシステムでは、リソースの種類よりもリソースの取得の方がはるかに多いため、『リソースの取得は初期化である』という手法は、『finally』構造を使用するよりもコードが少なくなります。」[ 1 ]
クラス不変条件として、RAII は、リソースを取得したはずのオブジェクト インスタンスが実際にリソースを取得していることを保証します。これにより、新しく作成されたオブジェクトを使用可能な状態にするための追加の「セットアップ」メソッドが不要になります (そのような作業はすべてコンストラクタで実行されます。同様に、リソースを解放する「シャットダウン」タスクはオブジェクトのデストラクタで実行されます)。また、インスタンスが適切にセットアップされていることを毎回使用前に検証する必要もなくなります。[ 16 ]
RAII設計は、マルチスレッドアプリケーションにおけるミューテックスロックの制御によく用いられます。この場合、オブジェクトは破棄される際にロックを解放します。RAIIを使用しない場合、デッドロックが発生する可能性が高く、ミューテックスをロックするロジックとロックを解除するロジックが分離されてしまいます。RAIIを使用すると、ミューテックスをロックするコードには、実行がRAIIオブジェクトのスコープを抜けたときにロックが解放されるというロジックが実質的に含まれます。
もう一つの典型的な例は、ファイルとのやり取りです。書き込み用に開かれたファイルを表すオブジェクトがあるとします。この場合、ファイルはコンストラクタで開かれ、実行がオブジェクトのスコープを抜けるときに閉じられます。どちらの場合も、RAIIは問題のリソースが適切に解放されることを保証するだけであり、例外安全性を維持することには依然として注意が必要です。データ構造やファイルを変更するコードが例外安全でない場合、ミューテックスのロックが解除されたり、データ構造やファイルが破損した状態でファイルが閉じられたりする可能性があります。
動的に割り当てられたオブジェクト(newC++ でメモリが割り当てられたオブジェクト)の所有権も RAII で制御でき、RAII (スタックベース) オブジェクトが破棄されるとオブジェクトが解放されます。この目的のために、C++11 標準ライブラリでは、単一所有オブジェクトと共有所有権を持つオブジェクト用のスマートポインタクラスが定義されています。同様のクラスは、C++98 の およびBoost ライブラリでも利用できます。std::unique_ptrstd::shared_ptrstd::auto_ptrboost::shared_ptr
また、RAII を使用すると、ネットワーク リソースにメッセージを送信できます。この場合、RAII オブジェクトは、コンストラクタの最後に、初期化が完了したときにソケットにメッセージを送信します。また、デストラクタの開始時、つまりオブジェクトが破棄される直前にもメッセージを送信します。このような構造は、クライアント オブジェクトで、別のプロセスで実行されているサーバーとの接続を確立するために使用されることがあります。
直接的なメモリ管理機能を持たない、あるいはその使用を推奨しない多くのプログラミング言語では、「disposeパターン」と呼ばれる同様のメカニズムが用いられ、disposeスコープの終了時にオブジェクトに対して関連するリソースのクリーンアップを実行するメソッドが呼び出されます。
ClangとGNUコンパイラコレクションの導入以前のC言語のバージョンでは、属性をC言語の非標準拡張として実装します。 [ 17 ]次のコードは、変数がスコープ外になったときに呼び出される指定されたデストラクタ関数で変数に注釈を付けます。defer[[gnu::cleanup]]
#include <stdio.h> #include <time.h>void writeLogFile () { const char * logFileName = "logfile.txt" ;[[ gnu :: cleanup ( fclosep )]] FILE * logFile = fopen ( logFileName , "w+" );time_t now = time ( NULL );fprintf ( logFile , "ログの開始時刻: %s、時刻: %s" , filename , ctime ( & now )); }この例では、コンパイラは関数が戻る前にfclosep呼び出されるように調整します。logFilewriteLogFile
C++では、オブジェクトの破棄はデストラクタによって直接行われます。C++では、クラスはスコープの終わりに達するとX自動的にデストラクタを呼び出します。disposeパターンは、C++におけるRAIIとほぼ同等です。~X()
import std ;using std :: ifstream ; using std :: string ;void readFile () { if ( ifstream reader { "story.txt" }; reader ) { string line ; while ( std :: getline ( reader , line )) { std :: println ( "{}" , line ); } } else { std :: println ( stderr , "Failed to open file" ); } // if/elseブロックの後でreaderは破棄されます}C# にusingは、オブジェクトが を実装している場合に使用できる -with-resources ブロックがあります。これは、リソースの末尾にあるメソッドSystem.IDisposableを呼び出します。Dispose()
using System ; using System.IO ;using ( StreamReader reader = new StreamReader ( "story.txt" )) { string line ; while ( ( line = reader.ReadLine ( )) != null ) { Console.WriteLine ( line ); } } //ここでリーダーは自動的に破棄されますJavaにtryは、オブジェクトがを実装している場合に使用できる-with-resourcesブロックがあります。これは、リソースの末尾にあるメソッドjava.lang.AutoCloseableを呼び出します。close()
import java.io.BufferedReader ; import java.io.FileReader ; import java.io.IOException ;try ( BufferedReader reader = new BufferedReader ( new FileReader ( "story.txt" )) ) { String line ; while ( ( line = reader.readLine ( ) ) ! = null ) { System.out.println ( line ) ; } } catch ( IOException e ) { e.printStackTrace ( ) ; }Pythonにはブロックがあり、オブジェクトがメソッドをwith実装している場合に使用できます。__enter____exit__
with open ( "story.txt" , "r" ) as file : print ( file.readline ( )) # ファイルの最初の行を出力これは、ロックなどのリソースを管理するためにも使用されます。
スレッドからロックをインポートbalance_lock : Lock = Lock () with balance_lock : # 重要なセクション: ここで口座残高を更新します...Rust では、オブジェクトが を実装している場合、カスタムのクリーンアップ ロジックを定義できます。これにより、オブジェクトがスコープを抜けた後にメソッドstd::ops::Dropが呼び出されます。これは によって手動で呼び出すこともできます。drop()std::mem::drop()
RAII は、スタックに割り当てられたオブジェクトによって (直接または間接的に) 取得および解放されるリソースに対してのみ機能し、静的なオブジェクトの寿命が明確に定義されています。 リソースを取得および解放するヒープに割り当てられたオブジェクトは、C++ を含む多くの言語で一般的です。RAII は、リソースを解放するデストラクタ (または同等のもの) をトリガーするために、ヒープベースのオブジェクトがすべての実行パスに沿って暗黙的または明示的に削除されることに依存しています。[ 18 ] : 8:27これは、すべてのヒープオブジェクトを管理するためにスマートポインタを使用し、循環参照されるオブジェクトには弱いポインタを使用することで実現できます。
C++ では、スタックの巻き戻しは、例外がどこかでキャッチされた場合にのみ確実に発生します。これは、「プログラム内で一致するハンドラが見つからない場合、terminate() 関数が呼び出されます。terminate() の呼び出し前にスタックが巻き戻されるかどうかは実装定義です (15.5.1)」 (C++03 標準、§15.3/9) によるものです。[ 19 ]オペレーティングシステムはプログラム終了時にメモリ、ファイル、ソケットなどの残りのリソースを解放するため、この動作は通常許容されます。
2018年のGamelabカンファレンスで、Jonathan Blowは、RAIIの使用はメモリ断片化を引き起こし、それがキャッシュミスを引き起こし、パフォーマンスが100倍以上低下する可能性があると主張した。[ 20 ]
Perl、Python(CPython実装)[ 21 ] 、PHP [ 22 ]は、参照カウントによってオブジェクトのライフタイムを管理しており、RAIIの使用を可能にしています。参照されなくなったオブジェクトはすぐに破棄またはファイナライズされて解放されるため、デストラクタまたはファイナライザはその時点でリソースを解放できます。ただし、このような言語では必ずしも慣用的ではなく、特にPythonでは推奨されていません(weakrefパッケージのコンテキストマネージャとファイナライザの使用が推奨されています)。
しかし、オブジェクトのライフタイムは必ずしもスコープに縛られるわけではなく、オブジェクトは非決定的に破棄されるか、まったく破棄されない可能性があります。そのため、スコープの終了時に解放されるべきリソースが意図せずリークする可能性があります。静的変数(特にグローバル変数)に格納されたオブジェクトは、プログラム終了時にファイナライズされない可能性があり、そのリソースは解放されません。たとえば、CPython はそのようなオブジェクトのファイナライズを保証していません。さらに、循環参照を持つオブジェクトは単純な参照カウンタでは回収されず、不定期間存続します。回収されたとしても(より高度なガベージコレクションによって)、破棄のタイミングと破棄の順序は非決定的です。CPython には、サイクルを検出してサイクル内のオブジェクトをファイナライズするサイクル検出器がありますが、CPython 3.4 より前のバージョンでは、サイクル内のオブジェクトにファイナライザがある場合、サイクルは回収されません。[ 23 ]