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の適用:
- コンストラクタ:
dataをnullptr、sizeを0で初期化し、リソースの初期状態を安全に設定します。 - コピーコンストラクタ: ソースオブジェクトから新しいメモリを
new int[size]で確保し、内容をディープコピーします。これにより、新しいarray_bagオブジェクトが独立したリソースを所有します。 - 代入演算子 (
operator=): 自己代入チェック (if (this != &src)) の後、既存のdataをdelete[]で解放し、ソースオブジェクトから新しいメモリをnew int[size]で確保して内容をディープコピーします。これにより、代入先のオブジェクトが新しいリソースを適切に取得・管理します。 - デストラクタ (
~array_bag()):delete[] data;を呼び出し、動的に確保された配列メモリを確実に解放します。 insert()およびclear(): これらのメソッドも、動的に新しい配列を確保したり(insert)、既存の配列を解放したり(clear)する際に、メモリ管理を適切に行っています。
- コンストラクタ:
- 結論:
array_bagは、動的メモリというリソースの取得と解放をコンストラクタとデストラクタ、およびコピー/代入操作に結びつけることで、RAIIの原則を完全に遵守しています。
4. tree_bag クラス (具体的な実装)
tree_bag は、二分探索木を内部データ構造として使用し、ツリーノードの動的確保・解放を通じてRAIIを適用しています。
- 所有するリソース:
node* treeが指すツリーのルートノードから派生するすべての動的に確保されたノード。 - RAIIの適用:
- コンストラクタ:
treeをnullptrで初期化し、リソースの初期状態を安全に設定します。 - コピーコンストラクタ:
copy_nodeというヘルパー関数を使って、ソースオブジェクトから再帰的に新しいツリー構造を構築し、新しいノードをnew node(value)で確保します。これにより、新しいtree_bagオブジェクトが独立したツリーリソースを所有します。 - 代入演算子 (
operator=): 自己代入チェックの後、既存のツリーをdestroy_treeで解放し、copy_nodeで新しいツリー構造を構築します。これにより、代入先のオブジェクトが新しいツリーリソースを適切に取得・管理します。 - デストラクタ (
~tree_bag()):destroy_tree(tree);を呼び出し、再帰的にツリー内のすべてのノードメモリを確実に解放します。 insert()およびclear():insertは新しいノードをnewで確保し、clearはdestroy_treeを用いてツリー全体を解放します。
- コンストラクタ:
- 結論:
tree_bagもまた、動的メモリ(ツリーノード)というリソースの取得と解放をコンストラクタとデストラクタ、およびコピー/代入操作に結びつけることで、RAIIの原則を完全に遵守しています。
5. searchable_array_bag および searchable_tree_bag クラス (具体的な実装)
これらのクラスは、array_bag / tree_bag と searchable_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の内容をコピーするものではなく、意味論的に代入が成立しません。しかし、setがbagの所有権を持たないため、deleteやnewを行うべきではないというRAIIの観点からは、リソースリークには繋がりません。問題は、参照という設計上の選択からくる代入の意味論にあります。 - デストラクタ (
~set() = default;):bagが参照であるため、デストラクタでbagが指すオブジェクトをdeleteする必要はありません。これは、setがbagの所有権を持たないというRAIIの原則に則っており、正しい設計です。
- コンストラクタ (
- 結論:
setクラスはsearchable_bagオブジェクトの所有権を持たず、その寿命を管理しません。したがって、setインスタンス自体のRAIIは、自身が所有するリソースがないため、シンプルになります。setを利用する側が、setが参照するsearchable_bagオブジェクトの寿命を適切に管理する必要があります(通常、setのライフタイムが参照先のsearchable_bagのライフタイムよりも短くなるようにする)。