RAII

RAIIResource 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_ptrboost::interprocess::unique_ptrが実装されている。また、参照カウント方式で所有権を共有するオブジェクトのスマートポインタクラステンプレートとして、Boost C++ライブラリboost::shared_ptrがある。これはC++11にてstd::shared_ptrとして標準化された。shared_ptrとともに利用する弱参照スマートポインタとして、それぞれboost::weak_ptrおよびstd::weak_ptrが存在する。そのほか、侵入型参照カウント方式のboost::intrusive_ptrLoki英語版ポリシーベースのLoki::SmartPtrCOMインターフェイスオブジェクト (IUnknown) の参照カウント管理に特化したATLATL::CComPtrなどがある。

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

C++での例

[編集]

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

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

[編集]

単純な例として、関数内で一時的な作業領域として配列を動的確保することを考える[1]。単純な方法では、以下のようにnew[]演算子を使用する。

void function1A(size_t count) {     double* array1 = NULL;     double* array2 = NULL;     try {         // 配列を動的に確保する。メモリ確保失敗により std::bad_alloc 例外がスローされる可能性がある。         array1 = new double[count]();         array2 = new double[count]();          // 動的に確保した配列をここで作業領域として使用する。         for (size_t i = 0; i < count; ++i) {             array1[i] = i * 0.1;             array2[i] = i * 0.1;         }         // ...          // 配列を使わなくなったので削除する。         delete[] array2;         delete[] array1;     }     catch (...) {         // 例外がスローされる場合に備える。         delete[] array2;         delete[] array1;         throw; // 例外の再送出。     } } 

これはC言語のmallocおよびfree関数による原始的な寿命管理手法に近い。もし動的に確保したメモリを削除する前に関数を抜けるとメモリリークしてしまうため、慎重に削除処理をひとつひとつ記述していく必要がある。動的にメモリ管理するオブジェクトの数が増えるにつれ、ソースコードのメンテナンスコストは増大していく。

一方、RAIIを利用した場合は以下のようになる。

// RAII を実現する配列ラッパークラス。 template<typename T> class ArrayWrapper {     size_t m_count;     T* m_data; public:     ArrayWrapper() : m_count(), m_data() {}     explicit ArrayWrapper(size_t count) : m_count(count), m_data(new T[count]()) {}     ~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[](size_t index) { return m_data[index]; }     const T& operator[](size_t index) const { return m_data[index]; }     // コピーは禁止とする。所有権の移動もサポートしない。 private:     ArrayWrapper(const ArrayWrapper&);     ArrayWrapper& operator=(const ArrayWrapper&); };  void function1B(size_t count) {     ArrayWrapper<double> array1(count);     ArrayWrapper<double> array2(count);      // 動的に確保した配列をここで作業領域として使用する。     for (size_t i = 0; i < count; ++i) {         array1[i] = i * 0.1;         array2[i] = i * 0.1;     }     // ...  } // RAII 変数 array1, array2 の属するブロックを抜ける。このとき array2, array1 の各デストラクタが順に呼ばれ、それぞれが内部で管理する配列メモリ領域は自動的に破棄される。 

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

#include <boost/scoped_array.hpp>  void function1C(size_t count) {     boost::scoped_array<double> array1(new double[count]());     boost::scoped_array<double> array2(new double[count]());      for (size_t i = 0; i < count; ++i) {         array1[i] = i * 0.1;         array2[i] = i * 0.1;     } } 

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

#include <memory>  void function1D(size_t count) {     std::unique_ptr<double[]> array1(new double[count]());     std::unique_ptr<double[]> array2(new double[count]());      for (size_t i = 0; i < count; ++i) {         array1[i] = i * 0.1;         array2[i] = i * 0.1;     } } 

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

コンストラクタからの例外送出とRAII

[編集]

コンストラクタの実行中、処理が最後まで完了する前に例外がスローされた場合、デストラクタが呼ばれない。そのため、コンストラクタで複数のリソースをnew/new[]してポインタ型のメンバー変数に格納し、デストラクタでdelete/delete[]するようなコードをうかつに書いてしまうとメモリリークの原因となる。

template<typename T> class DualArrayWrapper {     size_t m_count;     T* m_data1;     T* m_data2; public:     explicit DualArrayWrapper(size_t count) : m_count(count), m_data1(), m_data2() {         m_data1 = new T[count]();         m_data2 = new T[count]();         // m_data2 への代入右辺式が std::bad_alloc 例外をスローした場合、         // DualArrayWrapper のデストラクタが呼ばれず、         // m_data1 に割り当てたメモリがリークする。     }     ~DualArrayWrapper() {         delete[] m_data2;         delete[] m_data1;     } // 他のメンバーの実装は省略。 }; 

かといって、try-catchを駆使して例外をハンドリングするコードを逐一書いていくと、たちまちソースコードの記述量が膨れ上がってしまう。このような場合は、RAIIクラスをメンバー変数に使うことで簡潔に例外安全を達成できる。

#include <vector>  template<typename T> class DualArrayWrapper {     std::vector<T> m_data1;     std::vector<T> m_data2; public:     explicit DualArrayWrapper(size_t count): m_data1(count), m_data2(count) {         // m_data2 のコンストラクタが std::bad_alloc 例外をスローした場合、         // DualArrayWrapper のデストラクタは呼ばれないが、         // m_data1 のデストラクタは呼ばれるため、メモリリークしない。     } // デストラクタを明示的に記述する必要はなく、デフォルト生成されるもので十分となる。 }; 

単一のリソースを管理するクラスの場合は、デストラクタで明示的に解放することが許容される[2]

ファイルハンドルの管理

[編集]

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

#include <cstdio> #include <cassert> #include <stdexcept>  FILE* openFile(const char* fileName, const char* mode) {     FILE* fp = std::fopen(fileName, mode);     if (!fp) {         throw std::runtime_error("Failed to open file!");     }     return fp; }  void writeLine(FILE* fp, const char* strLine) {     assert(fp);     const int ret = std::fprintf(fp, "%s\n", strLine);     if (ret < 0) {         throw std::runtime_error("Failed to write data on file!");     } }  void function2A() {     FILE* fp1 = NULL;     FILE* fp2 = NULL;     try {         fp1 = openFile("test1.txt", "a");         fp2 = openFile("test2.txt", "a");         // ファイルの書き込みを行なう。         writeLine(fp1, "Test line for file#1.");         writeLine(fp2, "Test line for file#2.");         // ファイルを使い続ける。         // 何か問題が起こって関数を抜ける場合、return の前に fclose() を忘れずに呼ばなければならない。          // 明示的にファイルを閉じる必要がある。         std::fclose(fp1);         std::fclose(fp2);     }     catch (...) {         // 獲得したリソースがあれば返却する。         if (fp1) {             std::fclose(fp1);         }         if (fp2) {             std::fclose(fp2);         }         throw; // 例外の再送出。     } } 

一方、RAIIを利用した場合は以下のようになる。

class FileWrapper {     FILE* m_fp; public:     FileWrapper(const char* fileName, const char* mode)         : m_fp(std::fopen(fileName, mode)) { // ファイルハンドルでデータメンバーを初期化。         if (!m_fp) {             throw std::runtime_error("Failed to open file!");         }     }     ~FileWrapper() {         assert(m_fp);         std::fclose(m_fp);     }     void writeLine(const char* strLine) {         assert(m_fp);         const int ret = std::fprintf(m_fp, "%s\n", strLine);         if (ret < 0) {             throw std::runtime_error("Failed to write data on file!");         }     }     // コピーは禁止とする。所有権の移動もサポートしない。 private:     FileWrapper(const FileWrapper&);     FileWrapper& operator=(const FileWrapper&); };  void function2B() {     FileWrapper file1("test1.txt", "a");     file1.writeLine("Test line for file#1.");     FileWrapper file2("test2.txt", "a");     file2.writeLine("Test line for file#2."); } // 関数の途中で return したり、例外がスローされたりしても、RAII 変数の属するブロックを抜けた時点で確実にファイルハンドルは閉じられる。 

標準C++ライブラリでは、抽象化されたファイルストリーム管理用のRAIIクラスとして、std::basic_fstreamが用意されている。

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

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

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

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

[編集]

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

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

例えば、stlsoft::scoped_handle[3]は、(voidを含む)任意の型の解放関数を受け付け、また0でない広義の「ヌル」値(無効値)も受け付ける(Windowsのように複数の呼出規約が用いられる環境では、どんなものでも受け付ける)。

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

#define INVALID_HANDLE_VALUE  ((HANDLE)((LONG_PTR)-1)) 
#define INVALID_SOCKET  (SOCKET)(~0) 

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

#include <WinSock2.h> #include <cstdlib> #include <cassert> #include <cstring> #include <stdexcept> #include <iostream> #include <stlsoft/smartptr/scoped_handle.hpp>  // 3つの資源を同時に使う。 void testScopedHandle() {     // ファイルを開く。     // CreateFile() は失敗した場合 INVALID_HANDLE_VALUE を返す。ただし NULL もまた HANDLE としては一般的に無効値。     HANDLE hFile = ::CreateFileW(L"test.txt", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);     if (hFile == NULL || hFile == INVALID_HANDLE_VALUE) {         throw std::runtime_error("Failed to create file handle!");     }     else {         stlsoft::scoped_handle<HANDLE> cleanupFile(hFile, ::CloseHandle, INVALID_HANDLE_VALUE); // ファイルが確実に閉じられるようにする。          // TCP ソケットを作成。         // BSD ソケット API における socket() 関数の戻り値は int で、異常値は負数 (-1) となっており、0 は正常値のひとつ。         // Winsock もそれを踏襲している。         SOCKET socketDesc = ::WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);         if (socketDesc == INVALID_SOCKET) {             throw std::runtime_error("Failed to create socket descriptor!");         }         else {             stlsoft::scoped_handle<SOCKET> cleanupSocket(socketDesc, ::closesocket, INVALID_SOCKET); // ソケットが確実に閉じられるようにする。              void *mem = std::malloc(10000);             if (!mem) {                 throw std::bad_alloc();             }             else {                 stlsoft::scoped_handle<void*> cleanupMem(mem, std::free); // メモリが確実に解放されるようにする。                  // ここでメモリとソケットとファイルを使う。                  const LARGE_INTEGER dummy = {};                 if (!SetFilePointerEx(cleanupFile.get(), dummy, NULL, FILE_END)) {                     throw std::runtime_error("Failed to set file pointer to end!");                 }                  const char* text = "Test line.\r\n";                 const DWORD numOfBytesToWrite = static_cast<DWORD>(std::strlen(text));                 DWORD numOfBytesWritten = 0;                 if (!::WriteFile(cleanupFile.get(), text, numOfBytesToWrite, &numOfBytesWritten, NULL) || numOfBytesWritten != numOfBytesToWrite) {                     throw std::runtime_error("Failed to write data on file!");                 }                  // ...              } // mem はここで解放される。             mem = NULL;              // ソケットを自動的な管理から切り離す。             //SOCKET detachedVal = cleanupSocket.detach();             //assert(detachedVal == socketDesc);          } // socketDesc を RAII から切り離した場合、ここでは閉じられない。          //const int ecode = ::closesocket(socketDesc);         socketDesc = INVALID_SOCKET;          // 早期に hFile の資源を返却することもできる。         //cleanupFile.close();      } // hFile の資源を早期に返却した場合、ここでは返却されない。     hFile = INVALID_HANDLE_VALUE; }  int main() {     WSADATA wsaData = {};     const int ecode = ::WSAStartup(MAKEWORD(2, 2), &wsaData);     if (ecode == 0) {         try {             testScopedHandle();         }         catch (const std::exception& ex) {             std::cout << ex.what() << std::endl;         }         ::WSACleanup();     } } 

制約

[編集]

RAIIクラスでは解放関数が失敗すると問題になる。C++では言語の制約上デストラクタから例外を投げるのは良い考えではないため、デストラクタではすべての例外を握りつぶす必要がある。エラーコードによる通知も難しくなるため、結果として解放失敗の原因を上位層に通知することが難しくなる。そのためstlsoft::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ステートメントでは、同様の目的に__enter____exit__のメソッドを使う。

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

脚注

[編集]
  1. ^ gcc拡張およびC99標準規格では、関数を抜けた際に自動的に破棄される可変長配列をサポートするが、スタックオーバーフローの危険性がある。必要な長さが事前に明らかでない場合は、mallocやnewによるヒープへの動的確保を利用する。
  2. ^ How to: Design for exception safety | Microsoft Learn
  3. ^ STLSoft: scoped_handle Class Template Reference
  4. ^ CreateFileW function | Microsoft Docs
  5. ^ WSASocketW function | Microsoft Docs

参考文献

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

外部リンク

[編集]