18.4 RWCollectable の格納と取り出しについて
以下のグローバル関係を用いて、クラスの参照関係あるいはポインタ関係を格納して取り出す方法は、14.5.1節で説明しました。
Rwvostream& operator<<(RWvostream&, const RWCollectable&); RWFile& operator<<(RWFile&, const RWCollectable&); Rwvostream& operator<<(RWvostream&, const RWCollectable*); RWFile& operator<<(RWFile&, const RWCollectable*); Rwvistream& operator>>(RWvistream&, RWCollectable&); RWFile& operator>>(RWFile&, RWCollectable&); Rwvistream& operator>>(RWvistream&, RWCollectable*&); RWFile& operator>>(RWFile&, RWCollectable*&);
これらの関数がどのように機能するかを理解することは、RWCollectableで作業する上で大切です。まず、簡単に説明します。
初めてコレクションオブジェクトのどれかに対して左シフト演算子 << の 1 つを呼び出すとき、アイデンティティ辞書が内部的に作成されます。オブジェクトのアドレスが、その出力ファイルでの順序(例えば、1 番目、2 番目、6 番目など) と共に辞書に納められます。
いったんこれが完了すると、オブジェクトの仮想関数である saveGuts() が呼び出されます。これが仮想関数であるため、呼び出しは派生クラスによって使用される saveGuts() の定義に行きます これまで説明したように、saveGuts() の役目はオブジェクトの内部コンポーネントを格納することです。オブジェクトに RWCollectable を継承する他のオブジェクトが含まれている場合、そのオブジェクトの saveGuts() は含まれている各オブジェクトごとに operator<<() を再帰的に呼び出します。
その後の operator<<() の呼び出しでは、新しくアイデンティティ辞書は作成されず、すでに存在している辞書にオブジェクトのアドレスが格納されます。アドレスがすでに書き込まれているオブジェクトのアドレスと同一である場合は、saveGuts() は呼び出されません。その代わり、そのオブジェクトが以前のオブジェクトと同一であるという参照が書き込まれます。
コレクション全体が走査され、saveGuts() への最初の呼び出しが戻ると、アイデンティティ辞書が削除され、operator<<() への呼び出しが戻ります。
operator>>() 関数は、restoreGuts を呼び出してオブジェクトをストリームまたはファイルからメモリに復元し、この処理全体を本質的に逆に行います。既に作成されているオブジェクトへの参照に出会うと、この関数は、RWFactory に新しいオブジェクトの作成を要求するのではなく、単に旧オブジェクトのアドレスを返します。
以下は、これらの関数を使うより複雑な例です。
#include <rw/collect.h>
#include <rw/rwfile.h>
#include <assert.h>
class Tangle : public RWCollectable
{
public:
RWDECLARE_COLLECTABLE(Tangle)
Tangle* nextTangle;
int someData;
Tangle(Tangle* t = 0, int dat = 0){nextTangle=t; someData=dat;}
virtual void saveGuts(RWFile&) const;
virtual void restoreGuts(RWFile&);
};
void Tangle::saveGuts(RWFile& file) const{
RWCollectable::saveGuts(file); // 抽象基底クラスを保存する
file.Write(someData); // 内部を保存する
file << nextTangle; // 次のリンクを保存する
}
void Tangle::restoreGuts(RWFile& file){
RWCollectable::restoreGuts(file); // 抽象基底クラスを復元する
file.Read(someData); // 内部を復元する
file >> nextTangle; // 次のリンクを復元する
}
// ヘッド "p" を持つヌル区切りのリストの内部統合性を調べる
void checkList(Tangle* p){
int i=0;
while (p)
{
assert(p->someData==i);
p = p->nextTangle;
i++;
}
}
RWDEFINE_COLLECTABLE(Tangle, 100)
main(){
Tangle *head = 0, *head2 = 0;
for (int i=9; i >= 0; i--)
head = new Tangle(head,i);
checkList(head); // オリジナルのリストを調べる
{
RWFile file("junk.dat");
file << head;
}
RWFile file2("junk.dat");
file2 >> head2;
checkList(head2); // 復元したリストを調べる
return 0;
}
この例では、Tangle クラスが循環リンクリストを実装します。何が起こるでしょうか? 初めて operator<<() 関数が Tangle のインスタンスに呼び出されると、operator<<() 関数は前述したようにアイデンティティ辞書を設定し、Tangle の saveGuts() を呼び出します。ここで定義されているように、これは Tangle のメンバデータを格納し、次のリンクで operator<<() を呼び出します。この再帰性は、環状に継続します。
チェーンが nil オブジェクトで終わる場合 (すなわち、nextTangle がゼロ) の場合、operator<<() はこれを内部的に記録し、再帰を停止します。
その一方で、リストが環状である場合は、operator<<() が Tangle の最初にインスタンス (このチェーンすべてを開始したインスタンス)で再び呼び出されます。これが発生すると、operator<<() はこれはすでに処理したことがあるインスタンスであることを認識し、再び saveGuts() を呼び出すのではなく、前に書き込んだリンクへの参照を作成します。これにより、一連の再帰的な呼び出しが停止し、スタックが解放されます。
チェーンは、同様な方法で復元されます。次の呼び出しにより、
RWFile& operator>>(RWFile&, RWCollectable*&);
新しいオブジェクトがヒープ領域に作成され、そのオブジェクトへのポインタ、前に読み込んだオブジェクトのアドレス、あるいはヌルポインタが返されます。最後の 2 つの場合は、再帰が停止し、スタックが解放されます。