14.6 注意事項
永続性は、便利な性質ですが、ある面では特別の配慮が必要です。次に、オブジェクトで 永続性を使用するときに注意すべき事項を示します。
14.6.1 常にポインタによる同一オブジェクトを保存する前に、値による オブジェクトを保存すること
同形および多形永続性を持つオブジェクト両方において、値によるオブジェクトをストリームに出力してから、ポインタによる同一オブジェクトをストリームに出力してください。値とその値を指すポインタを含むクラスを設計するときはいつでも、そのクラスのメンバ関数 saveGuts と restoreGuts がまず値を保存あるいは復元し、それからポインタを保存あるいは復元するようにしてください。
次に、同形永続性を持つクラスを作成し、そのクラスのオブジェクトを具体化し、それらのオブジェクトに同形永続性を持たせようとする例を示します。 この例は失敗に終わります 。コードに続く説明を参照してください。
class Elmer {
public:
/* ... */
Elroy elroy_;
Elroy* elroyPtr_; // elroyPtr_ will point to elroy_.
};
RWDEFINE_PERSISTABLE(Elmer)
void rwSaveGuts(RWFile& file, const Elmer& elmer) {
// elroyPtr_ の値を作成し、ストリームに出力する:
file << *elroyPtr_;
// elroyPtr_ == &elroy_ の場合は、elroyPtr_ に
// 作成された値を指す elroy_ への参照を
// 格納する:
file << elroy_;
}
void rwRestoreGuts(RWFile& file, Elmer& elmer){
// elroyPtr_ の値をメモリに作成し、
// elroyPtr_ がメモリ内のその値を指すように
// 変更する:
file >> elroyPtr_;
// 値への参照を代入する
// elroyPtr_ == &elroy であれば
// elroy_ の値はすでに作成されているが、
// elroyPtr_ != &elroy なので
// RWTOOL_REF 例外処理が投げ入れられる:
file >> elroy_;
}
/* ... */
RWFile file("elmer.dat");
Elmer elmer;
Elmer elmer2;
elmer.elroyPtr_ = &(elmer.elroy_);
/* ... */
file << elmer; // 問題が起こりつつある...
/* ... */
file >> elmer2; // 問題が発生する RWTOOL_REF 例外処理!
/* ... */
上記コードでは、次の行は elmer を file に保存します。
file << elmer;
まず、この行は挿入演算子を呼び出します。
operator<<(RWFile&, const Elmer&)
elmer はまだ保存されていないので、elmer の値が file に保存され、その値が同形的保管表に追加されます。ところが、elmer はメンバ elroy_ および elroyPtr_ を持ち、これらは保存する elmer の一部として保存されなければなりません。elmer のメンバは、rwSaveGuts(RWFile&, const Elmer&)
に保存されます。
関数 rwSaveGuts(RWFile&, const Elmer&) は、まず elroyPtr_ を保存し、それから elroy_ を保存します。rwSaveGuts は、値 *elroyPtr_ を保存するときに挿入演算子 operator<<(RWFile&, const Elroy&) を呼び出します。
挿入演算子 operator<<(RWFile&, const Elroy&) は、elmer.elroyPtr_ がまだ格納されていないとみなし、*(elmer.elroyPtr_) の値を file に保存し、同形保管表にこの値がすでに格納されていることを書き留めます。そして、operator<<(RWFile&, const Elroy&) は、rwSaveGuts(RWFile&, const Elmer&) に戻ります。
rwSaveGuts(RWFile&, const Elmer&) に戻り、elmer.elroy_ が保存される時が来ました。 この例では、elmer.elroyPtr_ は elmer.elroy_ と同じアドレスを持ちます。再び挿入演算子 operator<<(RWFile&, const Elroy&) が呼び出されますが、今回は挿入演算子が、同 形保管表から *(elmer.elroyPtr_) がすでに格納されていることに気づき、値の代わりに *(elmer.elroyPtr_) への参照を file に格納します。
すべて問題ないように見えますが、実は問題が起こりつつあります。次の行で、問題が発生します。
file >> elmer2;
この行は、抽出演算子 operator>>(RWFile&, const Elmer&) を呼び出します。elmer2 はま だ復元されていないので、elmer2 の値が file から復元され、その値が同形的復元表に追加さ れます。elmer2 の値を抽出するためには、メンバ elroy_ と elroyPtr_ を抽出しなければな りません。elmer2
のメンバが、rwRestoreGuts に復元されます。
関数 rwRestoreGuts は、まず elroyPtr_ を復元し、それから elroy_ を復元します。 rwRestoreGuts は、値 *elroyPtr_ を復元し、抽出演算子 operator>>(RWFile&, Elroy*&) を呼び出します。
抽出演算子 operator>>(RWFile&, Elroy*&) は、elmer2.elroyPtr_ が file からまだ取り出されていないと見なします。そのため、抽出演算子は file から値 *(elmer2.elroyPtr_) を取り出し、メモリ領域を割り当てて、この値を作成し、この値を指すように elmer2.elroyPtr_ を更新します。次に、抽出演算子は、同形的復元表にこの値がすでに作成されていることを書き留めます。その後、operator>>(RWFile&, Elroy&) は rwRestoreGuts(RWFile&, Elmer&) に 戻ります。
rwRestoreGuts(RWFile&, Elmer&) に戻って、elmer2.elroy_ を復元するときがやってきます。elmer.elroyPtr_ と elmer.elroy_ は同じアドレスを持っていることを思い出してください。関数 rwRestoreGuts は抽出演算子 operator>>(RWFile&, Elroy&) を呼び出します。そしてこの抽出演算子は、同形的保管表を見て、*(elmer2.elroyPtr_) がすでに file から取り出 されていることに気づきます。値がすでに復元表に格納されているので、抽出演算子はファイルから値を取り出す代わりに、*(elmer.elroyPtr_) への参照を取り出します。ところが、抽出演算子は、elmer2.elroyPtr_ が復元表に置いた値のアドレスが elmer2.elroy_ のアドレスと 違うことに気づきます。そこで、operator>>(RWFile&, Elroy&) が RWTOOL_REF 例外処理を投 げ入れ、復元処理は中断されます。
この特定の問題は、次のように Elmer のメンバを保存および復元する順序を逆にすることによって、簡単に解決できます。以下が問題の部分です。
// 間違い!
void rwSaveGuts(RWFile& file, const Elmer& elmer) {
file << *elroyPtr_;
file << elroy_;
}
void rwRestoreGuts(RWFile& file, Elmer& elmer){
file >> elroyPtr_;
file >> elroy_;
}
関数を以下のように作成しなければなりません。
// 正しい!
void rwSaveGuts(RWFile& file, const Elmer& elmer) {
file << elroy_;
file << *elroyPtr_;
}
void rwRestoreGuts(RWFile& file, Elmer& elmer){
file >> elroy_;
file >> elroyPtr_;
}
ここに示したように、rwRestoreGuts と rwSaveGuts を訂正すれば、同形的保管表と復元表 は必要に応じてアドレス Elmer::elroy_ を使って Elmer::elroyPtr_ を更新します。
まとめ: 同じクラスにある値とその値を指すポインタの両方を持つ可能性があるため、常にポインタの前に値を操作するように、メンバ関数 rwSaveGuts と rwRestoreGuts を 定義することをお勧めします。
14.6.2 同じアドレスを持つ別個のオブジェクトを保存しないこと
同じアドレスを持つ可能性のある別個のオブジェクトを同形的に保存しないように注意しなければなりません。同形および多形永続性で使用される内部表は、オブジェクトのアドレスを使って、そのオブジェクトがすでに保存されているかどうかを判断します。
次の例は、Godzilla と Mothra が同形永続性を持つことを目的として作成されたコードです。
class Mothra {/* ... */};
RWDEFINE_PERSISTABLE(Mothra)
struct Godzilla {
Mothra mothra_;
int wins_;
};
RWDEFINE_PERSISTABLE(Godzilla)
/* ... */
Godzilla godzilla;
/* ... */
stream << godzilla;
/* ... */
stream >> godzilla; // 化けて復元される可能性あり!
godzilla が保存されるとき、godzilla のアドレスが同形的保管表に保存されます。次に保存される項目は、godzilla.mothra_ です。このアドレスも同じ内部保管表に保存されます。
問題は、あるコンパイラにおいては godzilla と godzilla.mothra_ が同じアドレスを持つ ことです。godzilla の復元時に、godzilla.mothra_ が値として取り出され、godzilla は godzilla.mothra_ への参照として取り出されます。godzilla と godzilla.mothra が同じアドレスを持つ場合、抽出演算子が godzilla を godzilla.mothra_ の内容で初期化しようとする ため、godzilla の復元に失敗します。
この問題に対処するには、2 つの方法があります。最初の方法は、int などの単純データメンバが、同形永続性を持つデータメンバに先行するクラスを構成することです。この方法を用 いると、クラス Godzilla は次のようになります。
struct Godzilla {
int wins_;
Mothra mothra_; // これで、mothra_ は別のアドレスを持つ
};
Godzilla がここに示されているように構成されると、mothra_ は godzilla の後に転置され、godzilla と間違えられることがありません。整数型の変数 wins_ は、単純永続性で保存 され、同形保管表には格納されません。
クラスとそのメンバの同一アドレスの問題を解決するもう一つの方法は、同形永続性を持つメンバを値としてではなくポインタとして挿入することです。この場合、Godzilla は次のようになります。
struct Godzilla {
Mothra* mothraPtr_;// mothraPtr_ は別のアドレスを指す
int wins_;
};
2 番目の方法では、mothraPtr_ は godzilla とは異なるアドレスを指すので、混乱を避けることができます。
14.6.3 異種の RWCollectable を格納するために RWCollection をソー トしないこと
RWCollection に複数の型の RWCollectable が格納されている場 合は、RWCollection をソートできません。例えば、RWCollectableString と RWCollectableDate を同一の RWCollection に格納する予定の場合、RWBtree などのソート済みの RWCollection コレクションに格納できないことを意味します。ソート済みの RWCollection には、RWBinaryTree、RWBtree 、RWBTreeDictionary、および RWSortedVector があります。
この制約の理由は、ソート済みの RWCollection の比較関数は比較されるオブジェクトが同じ型であることを期待するためです。
14.6.4 復元する RWCollectable すべてを定義すること
復元する可能性のある RWCollectable オブジェクトすべてが プログラムで宣言されていることを確認してください。この具体例には、14.5.3.2 節を参照してください。
RWCollectable オブジェクトをコレクションに保存し、保存されている RWCollectable を使わずに、別のプロ グラムでコレクションを復元することによって多形永続性を利用する場合、特にこれらの宣言が大切になります。復元の試行中に適切な変数を宣言しないと、RWFactory が存在すると分かっている RWCollectable クラス ID の RW_NOCREATE 例外処理を投げ入れます。多形的 に復元できる RWCollectables すべての変数が宣言されている場合、 RWFactory は、RW_NOCREATE 例外処理を投げ入ることはありません。
コンパイラのリンカが、コード内で RWCollectable が特に記述されている ときに、RWFactory が足りない RWCollectable を作成するのに必要な コードだけをリンクするので、問題が発生します。足りない RWCollectable を 宣言することにより、RWFactory が必要とする適切なコードをリンクするために必要な情報がリンカに与えられます。