Skip to content

RAII (Resource Acquisition Is Initialization) 観点からのクラス設計思想

1. RAIIとは

RAII (Resource Acquisition Is Initialization) は、C++におけるリソース管理のための強力なプログラミングイディオムです。この原則では、リソースの取得(メモリの確保、ファイルハンドルのオープン、ネットワーク接続の確立など)をオブジェクトのコンストラクタと結びつけ、リソースの解放をオブジェクトのデストラクタと結びつけます。

これにより、以下の利点が得られます。

  • リソースリークの防止: オブジェクトがスコープを抜ける際に、デストラクタが自動的に呼び出され、リソースが確実に解放されます。
  • 例外安全性: 例外が発生した場合でも、スタック巻き戻し中にデストラクタが呼び出されるため、リソースリークを防ぎます。
  • コードの簡潔さと保守性: 手動でのリソース解放コードを減らし、コードをより読みやすく、エラーを起こしにくくします。

RAIIの核心は、オブジェクトが「リソースの所有権」を持つことにあります。オブジェクトがリソースの所有者であれば、そのリソースの寿命を管理する責任を負います。

2. bag および searchable_bag クラス (抽象基底クラス/インターフェース)

これらのクラスは、純粋仮想関数を持つ抽象基底クラス(インターフェース)として設計されています。

  • リソース所有権: 自身では具体的なリソース(動的メモリなど)を直接所有したり管理したりすることはありません。
  • RAIIとの関連:
    • virtual ~bag() = default; および virtual ~searchable_bag() = default; のように仮想デストラクタが定義されています。これは、派生クラスのオブジェクトが基底クラスのポインタを介して削除される際、適切な派生クラスのデストラクタが呼び出されることを保証するために不可欠です。この仕組みにより、派生クラスでRAIIが正しく適用されている場合に、そのリソースが確実に解放されます。

3. array_bag クラス (具体的な実装)

array_bag は、動的に確保される整数配列を内部データ構造として使用しており、RAIIを直接適用しています。

  • 所有するリソース: int* data が指す動的配列。
  • RAIIの適用:
    • コンストラクタ: datanullptrsize0 で初期化し、リソースの初期状態を安全に設定します。
    • コピーコンストラクタ: ソースオブジェクトから新しいメモリを new int[size] で確保し、内容をディープコピーします。これにより、新しい array_bag オブジェクトが独立したリソースを所有します。
    • 代入演算子 (operator=): 自己代入チェック (if (this != &src)) の後、既存の datadelete[] で解放し、ソースオブジェクトから新しいメモリを new int[size] で確保して内容をディープコピーします。これにより、代入先のオブジェクトが新しいリソースを適切に取得・管理します。
    • デストラクタ (~array_bag()): delete[] data; を呼び出し、動的に確保された配列メモリを確実に解放します。
    • insert() および clear(): これらのメソッドも、動的に新しい配列を確保したり(insert)、既存の配列を解放したり(clear)する際に、メモリ管理を適切に行っています。
  • 結論: array_bag は、動的メモリというリソースの取得と解放をコンストラクタとデストラクタ、およびコピー/代入操作に結びつけることで、RAIIの原則を完全に遵守しています。

4. tree_bag クラス (具体的な実装)

tree_bag は、二分探索木を内部データ構造として使用し、ツリーノードの動的確保・解放を通じてRAIIを適用しています。

  • 所有するリソース: node* tree が指すツリーのルートノードから派生するすべての動的に確保されたノード。
  • RAIIの適用:
    • コンストラクタ: treenullptr で初期化し、リソースの初期状態を安全に設定します。
    • コピーコンストラクタ: copy_node というヘルパー関数を使って、ソースオブジェクトから再帰的に新しいツリー構造を構築し、新しいノードを new node(value) で確保します。これにより、新しい tree_bag オブジェクトが独立したツリーリソースを所有します。
    • 代入演算子 (operator=): 自己代入チェックの後、既存のツリーを destroy_tree で解放し、copy_node で新しいツリー構造を構築します。これにより、代入先のオブジェクトが新しいツリーリソースを適切に取得・管理します。
    • デストラクタ (~tree_bag()): destroy_tree(tree); を呼び出し、再帰的にツリー内のすべてのノードメモリを確実に解放します。
    • insert() および clear(): insert は新しいノードを new で確保し、cleardestroy_tree を用いてツリー全体を解放します。
  • 結論: tree_bag もまた、動的メモリ(ツリーノード)というリソースの取得と解放をコンストラクタとデストラクタ、およびコピー/代入操作に結びつけることで、RAIIの原則を完全に遵守しています。

5. searchable_array_bag および searchable_tree_bag クラス (具体的な実装)

これらのクラスは、array_bag / tree_bagsearchable_bag を多重継承しています。

  • リソース所有権: 自身では新たな動的リソースを直接所有したり管理したりすることはありません。リソースの所有権と管理は、基底クラスである array_bag または tree_bag に委譲されています。
  • RAIIの適用:
    • コンストラクタ、コピーコンストラクタ、代入演算子、デストラクタは、それぞれ基底クラスの対応するメンバを呼び出すか、デフォルト実装に任せられています。例えば、~searchable_array_bag() = default; のデストラクタは、array_bag のデストラクタが自動的に呼び出されることを保証し、リソースの適切な解放が行われます。
  • 結論: 親クラスがRAIIに基づいたリソース管理を適切に行っているため、これらの派生クラスは追加のリソース管理ロジックを持つ必要がなく、RAIIの原則に従っています。

6. set クラス (コンポジション - 参照メンバー)

set クラスは private: searchable_bag& bag; という参照メンバーを持つことで、他の searchable_bag オブジェクトを「利用」しますが、「所有」はしません。

  • リソース所有権: set クラスは bag 参照が指す searchable_bag オブジェクトの所有権を持たないため、その寿命を管理する責任も負いません。
  • RAIIの適用:
    • コンストラクタ (set(searchable_bag& b)): 外部から提供された searchable_bag オブジェクトへの参照を初期化リストで受け取ります。リソースを「取得」するのではなく、「参照」するだけです。
    • コピーコンストラクタ (set(const set& other)): 他の set オブジェクトが参照している bag と同じ bag への参照を初期化します。ここでも所有権は発生しません。
    • 代入演算子 (operator=): 参照は一度初期化されると再代入できないため、この代入演算子は bag メンバーの参照先を切り替えることはできません。現在の実装 (bag.clear();) は、参照先の searchable_bag の内容をクリアしようとしますが、代入元 other の内容をコピーするものではなく、意味論的に代入が成立しません。しかし、setbag の所有権を持たないため、deletenew を行うべきではないというRAIIの観点からは、リソースリークには繋がりません。問題は、参照という設計上の選択からくる代入の意味論にあります。
    • デストラクタ (~set() = default;): bag が参照であるため、デストラクタで bag が指すオブジェクトを delete する必要はありません。これは、setbag の所有権を持たないというRAIIの原則に則っており、正しい設計です。
  • 結論: set クラスは searchable_bag オブジェクトの所有権を持たず、その寿命を管理しません。したがって、set インスタンス自体のRAIIは、自身が所有するリソースがないため、シンプルになります。set を利用する側が、set が参照する searchable_bag オブジェクトの寿命を適切に管理する必要があります(通常、set のライフタイムが参照先の searchable_bag のライフタイムよりも短くなるようにする)。