株式会社エス・スリー・フォー

14.5 多形永続性

多形永続性は、永続性のあるオブジェクト間のポインタ関係 (参照関係) を保存し、オブジェクトの復元プロセスにそのオブジェクトの型を事前に知らなくても、オブジェクトを復元することができます。

Tools.h++ では、多形永続性を持たせるために RWCollectable から派生したクラスを使います。これらのクラスから作成されたオブジェクトは、RWCollectable から派生した型のどれにでもなることができます。異なる型のオブジェクトから成るグループを、異種コレクションと呼びます。

表 7 は、多形永続性を使用するクラスを示したものです。

表 3. 多形永続性を持つクラス

分類
説明
RWCollectable (Smalltalk_like) クラス
RWCollectableDate, RWCollectableString…
RWCollection クラスから派生した RWCollectable クラス
RWBinaryTree, RWBag …

14.5.1 演算子

Tools.h++ クラスライブラリの強力で適応性のある機能の 1 つに、RWCollectable を継承する多形永続性のオブジェクトの格納と取り出しがあります。他の永続性機構と同様に、多形永続性は多重定義された挿入および抽出演算子 (operator<< と operator>>) を使用します。これらの演算子を多形永続性で使用すると、オブジェクトを同形永続性で保存し復元するだけなく、型が不明のオブジェクトも復元できます。

多形永続性は、以下の演算子を使用します。

  • RWCollectable オブジェクトへの参照を保存する演算子:

    Rwvostream&  operator<<(RWvostream&, const RWCollectable&);
    RWFile&      operator<<(RWFile&,     const RWCollectable&);
    

    RWCollectable から派生した各オブジェクトは、オブジェクトのクラスを一意に識別するクラス ID で同形永続的に保存されます。

  • RWCollectable ポインタを保存する演算子:

    Rwvostream&  operator<<(RWvostream&, const RWCollectable*);
    RWFile&      operator<<(RWFile&,     const RWCollectable*);
    

    オブジェクトを指す各ポインタは、オブジェクトのクラスを一意に識別するクラス ID で同形永続的に保存されます。また、nil ポインタも保存できます。

  • 既存のRWCollectable オブジェクトを復元する演算子

    Rwvistream&  operator>>(RWvistream&, RWCollectable&);
    RWFile&      operator>>(RWFile&,     RWCollectable&);
    

    RWCollectable から派生した各オブジェクトは、同形永続的に復元されます。永続性機構は、オブジェクトと一緒に格納されたクラス ID を調べることにより、実行時にオブジェクトの型を判断します。

  • RWCollectable オブジェクトを指すポインタを復元する演算子

    Rwvistream&  operator>>(RWvistream&, RWCollectable*&);
    RWFile&      operator>>(RWFile&,     RWCollectable*&);
    

    RWCollectable から派生した各オブジェクトは同形的に復元され、ポインタ参照は復元されたオブジェクトを指すように更新されます。永続性機構は、オブジェクトと一緒に格納されたクラス ID を調べることにより、実行時にオブジェクトの型を判断します。復元されたオブジェクトはヒープ領域に割り当てられているため、作業が終わったらそれらのオブジェクトを削除するのはユーザの責任です。

14.5.2 多形永続性を持つように独自のクラスを設計する場合

多形性のあるオブジェクトのポインタを復元する機能は、基底クラス RWCollectable のプロパティです。RWCollectable から派生したオブジェクトならば、独自のクラスを含め、どんなオブジェクトでも多形永続性を使用できます。第 15 章「RWCollectable クラスの設計」では、RWCollectable を継承することによって作成したクラスに多形永続性の実装する方法について説明します。

14.5.3 多形永続性例

多形永続性の例では、2 つの別々のプログラムを示します。最初の例では、コレクションの内容を標準出力 (stdout) に多形的に保存します。2 番目の例では、保存されているコレクションの内容を標準入力 (stdin) から多形的に復元します。永続性を使って異なる 2 つの処理間でオブジェクトを共有できることを示すために、例を 2 つに分けました。

最初の例をコンパイルして実行すると、ファイルに格納されるオブジェクトが出力されます。ただし、最初の例の出力を 2 番目の例にパイプ接続することができます。

firstExample | secondExample

14.5.3.1 例 1: 多形的保存

この例では、空のコレクションを作成し、オブジェクトをそのコレクションに挿入して、コレクションを標準出力に多形的に保存します。

例 1 では、同じオブジェクトの 2 つのコピーとその他の 2 つのオブジェクトを含むコレクションが作成、保存されることに注意してください。4 個のオブジェクトは 3 つの異なる型を持ちます。例 1 でコレクションが保存されるとき、および例 2 でコレクションが復元されるとき、

  • コレクションの参照関係が保持されていることがわかります。
  • コレクションを復元するプロセスでは、そのオブジェクトを復元するまでオブジェクトの型が分かりません。

以下が例 1 のコードです。

#include <rw/ordcltn.h>
#include <rw/collstr.h>
#include <rw/collint.h>
#include <rw/colldate.h>
#include <rw/pstream.h>

main(){
   // 空のコレクションを生成する
   RWOrdered collection;

   // オブジェクトをコレクションに挿入する

   RWCollectableString* george;
   george = new RWCollectableString("George");

   collection.insert(george);     // 文字列を 1 度追加する
   collection.insert(george);     // 文字列を 2 度追加する
   collection.insert(new RWCollectableInt(100));
   collection.insert(new RWCollectableDate(3, "May", 1959));

   // 移植可能なストリームを使って cout に格納する
   RWpostream ostr(cout);
   ostr << collection;
      // 上の行は挿入演算子を呼び出す:
      //    Rwvistream&
      //      operator<<(RWvistream&, const RWCollectable&);

   // ここでコレクション内のメンバすべてを削除する
   // clearAndDestroy() は各オブジェクトが 1 度だけ削除されるように
   // 作成されているので、同じオブジェクトを何回も削除することを
   // 心配する必要はない

   collection.clearAndDestroy();

   return 0;
}

collection に格納されるオブジェクトには、RWCollectableDate、RWCollectableInt、RWCollectableString (2 個のオブジェクト)の 3 つの型があることに注意してください。RWCollectableString 型の george は collection に 2 度挿入されています。

14.5.3.2 例 2: 多形的に復元する

2 番目の例では、多重定義された抽出演算子を使って、最初の例で多形的に保存したコレクションをどのように読み戻し、忠実に復元するかを示します。

Rwvistream&  operator>>(RWvistream&, RWCollectable&);

この例では、プログラムが次のステートメントを実行するときに永続性が発生します。

   istr >> collection2;

この行は、多重定義された抽出演算子を使って、最初の例で collection2 に保存したコレクションを同形的に復元します。

永続性はどのように発生するのでしょうか? 入力ストリーム istr から collection2 に格納されている RWCollectable の派生オブジェクトを指す各ポインタごとに、抽出演算子 operator>> は各種の多重定義された抽出演算子と永続性関数を呼び出します。RWCollectable の派生オブジェクトを指すポインタごとに、collection2 の抽出演算子が次のことを行います。

  • ストリーム istr を読み取り、RWCollectable の派生オブジェクトの型を見つけます。
  • ストリーム istr を読み取り、ポインタが指している RWCollectable 派生オブジェクトがすでに復元されていて、復元表で参照されているかどうかを調べます。
    • RWCollectable の派生オブジェクトがまだ復元されていない場合は、抽出演算子がポインタを作成し、正しい型のオブジェクトをヒープ領域に作成し、作成されたオブジェクトをストリームから読み取ったデータで初期化します。次に、演算子はポインタを新しいオブジェクトのアドレスで更新します。
    • RWCollectable 派生オブジェクトがすでに復元されている場合は、抽出演算子がポインタを作成し、ストリームからオブジェクトへの参照を読み出します。次に、演算子はその参照を使って、復元表からオブジェクトのアドレスを取得し、ポインタをそのアドレスで更新します。
  • 最後に、復元されたポインタをコレクションに挿入します。

永続性機構の実装について詳しくは、14.5.3.3 節で再び検討します。ただし、異種コレクション (RWCollection を基底とする必要がある) が復元されるとき、復元プロセスは復元するオブジェクトの型を判断できません。そのため、常にオブジェクトにヒープ領域を割り当てる必要があります。つまり、復元された内容を削除するのは、ユーザの責任であることを意味します。これは、例の最後にある式 collection2.clearAndDestroy で実行されます。

次にコードを示します。

#define RW_STD_TYPEDEFS
#include <rw/ordcltn.h>
#include <rw/collstr.h>
#include <rw/collint.h>
#include <rw/colldate.h>
#include <rw/pstream.h>

main(){
   RWpistream istr(cin);
   RWOrdered collection2;

   // このプログラムが復元しているものが何であるか
   // あらかじめ知る必要がなくても、
   // RWFactory の使うコードがリンクされるように、
   // リンカは可能性のあるものを知る必要がある。
   // RWFactory はクラスの ID を基にして RWCollectable オブジェクトを
   // 作成する

       RWCollectableInt   exemplarInt;
       RWCollectableDate  exemplarDate;

   // コレクションを読み戻す
       istr >> collection2;

   // 注意: 上の行は、コレクションを復元する
   // コードです。これ以降は、コレクションに何があるかを
   // 示すもの。

   // 同じ値を持つ文字列を検索するために、
   // "George" という値を持つテンポラリ文字列を作成する。
       RWCollectableString temp("George");

   // "George" を見つける:
   //   値 "George" があるかどうか
   //   collection2 を検索する。
   //   ポインタ "g" がその文字列を指す:
         RWCollectableString* g;
         g = (RWCollectableString*)collection2.find(&temp);

   // これで、"g" が値 "George" を持つ文字列を指す
   // コレクションに g が何個あるか?

   size_t georgeCount   = 0;
   size_t stringCount   = 0;
   size_t integerCount  = 0;
   size_t dateCount     = 0;
   size_t unknownCount  = 0;

   // 反復子を作成する:
       RWOrderedIterator sci(collection2);
       RWCollectable* item;

   // 反復子が、項目ごとにコレクションを一巡して、
   // 各項目ごとにポインタを返す

       while ( item = sci() ) {

         // このポインタが g と等値であるかどうかを調べる。
         // すなわち、等値性だけでなく、アイデンティティを調べる。
             if (item->isA() == __RWCOLLECTABLESTRING && item==g)
               georgeCount++;

         // 文字列、日付、整数を数える:
             switch (item->isA()) {
               case __RWCOLLECTABLESTRING: stringCount++; break;
               case __RWCOLLECTABLEINT:    integerCount++; break;
               case __RWCOLLECTABLEDATE:   dateCount++; break;
               default:                    unknownCount++; break;
             }
   }

   // 出力結果:
       cout << "There are:\n\t"
         << stringCount   << " RWCollectableString(s)\n\t"
         << integerCount  << " RWCollectableInt(s)\n\t"
         << dateCount     << " RWCollectableDate(s)\n\t"
         << unknownCount  << " other RWCollectable(s)\n\n"
         << "There are "
         << georgeCount
         << " pointers to the same object \"George\"" << endl;

   // 作成されたオブジェクトをすべて削除して、戻る:
       collection2.clearAndDestroy();
       return 0;
}

出力:
There are:
        2 RWCollectableString(s)
        1 RWCollectableInt(s)
        1 RWCollectableDate(s)
        0 other RWCollectable(s)

There are 2 pointers to the same object "George"

図 8 は、最初の例で作成され、2 番目の例で復元されたコレクションを示したものです。保存されたコレクションと復元されたコレクションで、メモリマップとデータ型の両方が同一であることに注意してください。

図 8. 多形永続性

image17
保存される前のコレクション (collection1)

image18
復元されたコレクション (collection2)

14.5.3.3 例 2 の詳細

多形永続性を実装するために使用した機構が分かるように、ここで例 2 を再び検討してみましょう。次の式

istr >> collection2;

は、多重定義されている抽出演算子を呼び出します。

RWvistream& operator>>(RWvistream& str, RWCollectable& obj);

抽出演算子はオブジェクトの仮想関数 restoreGuts() を呼び出すように作成されています。この場合、オブジェクト obj は順序付きコレクションで、このオブジェクトの restoreGuts() は、コレクションの各メンバごとに

RWvistream& operator>>(RWvistream&, RWCollectable*&);

を一度ずつ呼び出すように作成されています。[1] 2 番目の引数が単なる参照ではなく、ポインタへの参照であることに注意してください。このバージョンでは、多重定義された operator>> は、ストリームをみて、ストリーム上のオブジェクトの種類を判断し、その種類のオブジェクトをヒープ領域に割り当て、ストリームからオブジェクトを復元し、最後にそのオブジェクトへのポインタを返します。この operator>> が前のオブジェクトへの参照に出会うと、演算子は古いアドレスを返します。これらのポインタは、順序付きコレクションの restoreGuts() によってコレクションに挿入されます。

多形永続性の機構に関するこれらの詳細は、第 15 章「RWCollectable クラスの設計」で説明するように、独自の多形永続性を持つクラスを設計するときに特に重要です。このようなクラスで作業をする場合は、Smalltalk 類同コレクションが復元されるとき、復元されるオブジェクトの型はまったく分かっていないことに注意してください。そのため、復元プロセスでは、常にオブジェクトにヒープ領域を割り当てる必要があります。つまり、復元された内容を削除するのは、ユーザの責任であることを意味します。多形永続性の 2 つの例の最後に、その例が示されています。

14.5.3.4 使用する永続性演算子の選択

2 番目の例では、永続性演算子が、RWCollectable への参照にコレクションを復元しました。

Rwvistream&  operator>>(RWvistream&, RWCollectable&);

これは、次のような RWCollectable への参照を指すポインタではありません。

Rwvistream&  operator>>(RWvistream&, RWCollectable*&);

コレクションはスタック上に割り当てられました。

RWpistream istr(cin);
RWOrdered collection2;
istr >> collection2;
...
collection2.clearAndDestroy();

このとき、次のように operator>>(RWvistream&,RWCollectable*&) を使ってコレクションにメモリを割り当てることはしませんでした。

RWpistream istr(cin);
RWOrdered* pCollection2;
istr >> pCollection2;
...
collection->clearAndDestroy();
delete pCollection2;

なぜこのようにしたのでしょうか? 復元するコレクションの型が分かっている場合は、通常それを割り当てて、次のように復元する方法を取ってください。

Rwvistream&  operator>>(RWvistream&, RWCollectable&);

参照演算子を使うことにより、永続性機構がオブジェクトの型を判断するのに必要な時間を削除し、RWFactory に型を割り当てさせます (15.2.7 節「RWFactory について」を参照)。さらに、コレクションにメモリ領域を割り当てることにより、必要に応じた形で割り当てをカスタマイズできます。例えば、コレクションクラスの初期容量を設定できます。

注釈

  1. ^ Smalltalk 類同コレクションクラスは非常に似通っているので、これらのクラスはすべて RWCollection を継承したバージョンのrestoreGuts() を共有します。