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

6章: 仮想ストリームの使い方

続きを見る

4 章: クラス RWDate の使い方

続きを見る

11.11 移行ガイド

このマニュアルの第 1 章「Tools.h++ の紹介」で説明したように、このバージョンの
Tools.h++ の主な目標の 1
つは、以前のバージョンのライブラリをもとにした既存のコードを再利用できるようにすることにあります。この章を見てわかるように、コレクションクラステンプレートは最新の標準
C++ ライブラリと適合するように大きく再設計されています。次に再設計されたクラスを示します。

  • RWTPtrDlist
  • RWTValDlist
  • RWTPtrHashDictionary
  • RWTValHashDictionary
  • RWTPtrHashSet
  • RWTValHashSet
  • RWTPtrHashTable
  • RWTValHashTable
  • RWTPtrOrderedVector
  • RWTValOrderedVector
  • RWTPtrSlist
  • RWTValSlist
  • RWTPtrSortedVector
  • RWTValSortedVector

これらのクラスはすべて、標準 C++
ライブラリの有無にかかわらず使用できることは先に説明しました。標準 C++
ライブラリなしで使用する場合、これらのクラスは、バグがいくつか訂正されているものの、前のバージョンの
Tools.h++
と同じインタフェースと実装を持ちます。これらのわずかな変更のために、既存コードとソースの互換性で問題が起きることはほとんどありません。

ただし、このクラスを標準 C++
ライブラリと一緒に使用する場合は、既存のソースコードにいくつかの変更を加えなければならない場合があります。特定のクラスで必要な調整については、以下に説明します。

追加のテンプレート引数が必要になったクラス:
  • RWTPtrHashDictionary
  • RWTValHashDictionary
  • RWTPtrHashSet
  • RWTValHashSet
  • RWTPtrHashTable
  • RWTValHashTable
  • RWTPtrSortedVector
  • RWTValSortedVector

これらのテンプレートを使用する既存のコードでは、標準 C++
ライブラリと共に使用するときにこのバージョンの Tools.h++
が必要とするテンプレート引数の数が指定されていません。この問題を解決するためには、
11.10.1
節で説明したマクロを使用してください。マクロを使用すると、コンパイルが通り、既存コードの意味論が保持されます。

クラス階層が変更されたクラス:
  • RWTPtrHashSet
  • RWTValHashSet
  • RWTPtrHashTable
  • RWTValHashTable
  • RWTPtrOrderedVector
  • RWTValOrderedVector
  • RWTPtrSortedVector
  • RWTValSortedVector

コレクションクラステンプレートの内の継承関係が既存コードに使用されている場合、標準 C++
ライブラリと共にこのバージョンの Tools.h++
を使用しても、そのコードはコンパイルされません。標準 C++
ライブラリのコレクションクラステンプレートの実装には、継承関係がありません。例えば、前のバージョンの
Tools.h++ では、RWTPtrHashSet
RWTPtrHashTable を、RWTValOrderedVector
RWTValVector を、そして
RWTValSortedVectorRWTValOrderedVector
を継承しています。これらのテンプレートのポインタベースのバージョンも同じようなパターンに従っています。これらの関係は、新バージョンの
Tools.h++
では保持されていません。この継承をもとにしたコードがある場合は、コードを変更する必要があります。

既存のコードでこの問題が発生する可能性のある場所は、RWTValHashSet から
RWTValHashTableIterator を、または
RWTPtrHashSet から
RWTPtrHashTableIterator を構築する箇所です。
RWTValHashTableIterator
のコンストラクタは、継承関係に依存するのではなく RWTValHashSetを利用して、
RWTValHashTable への参照を行うためです。

この特定の問題を解決するには、RWTPtrHashSetIterator を使用します。RWTValHashSet から RWTValHashTableIterator
を生成するコードを見つけたら、その代わりに RWTValHashSetIterator を使用してください。RWTValHashSetIterator は、標準 C++
ライブラリの有無にかかわらず用意されているので、あとで標準 C++
ライブラリベースの実装に移行する予定がある場合でも、すぐに変更することができます。

項目型に必要な less-than 意味論が影響を受ける可能性のあるクラス
  • RWTPtrDlist
  • RWTValDlist
  • RWTPtrOrderedVector
  • RWTValOrderedVector
  • RWTPtrSlist
  • RWTValSlist
  • RWTPtrSortedVector
  • RWTValSortedVector

前述したように、コンパイラの中には、項目型の 2 つのインスタンスに対して式 (t1
< t2)

が定義されていることを要求するものがあります。これは、使用されているかどうかにかかわらず、すべてのメンバ関数を具体化する特定のコンパイラと連合して、sort()
min_element()
などの便利なメンバ関数を含むためです。operator<()
が定義されていない型 T
でこれらのテンプレートのどれかを具体化するコードがあるかもしれません。その場合には、operator<()
を定義しなければなりません。

operator<()
を必要とするメンバ関数を実際に使用する場合は、最適の方法は実際に使用するやり方でそれを定義することです。手っ取り早い方法は、コンパイラを満足させるようなダミーの
operator<()
をグローバルに定義することです。しかし、「コンパイラを満足させる」ためだけに書かれたコードは、コードの保守管理上、悪夢のような状態を引き起こすことがかなりあるため、可能なかぎりこれは避けてください。

 

11.10 標準ライブラリなしでテンプレートを使用する場合

RWTValVector<T>RWTPtrVector<T>RWTIsvSlist<T>、および RWTIsvDlist<T> など、Tools.h++ テンプレートのいくつかは、標準 C++ ライブラリをもとにしていません。認定済みプラットフォームならばどれでも、これらのテンプレートを使用できます。「はじめに」で説明したように、いわゆる標準ライブラリベースのテンプレートの多くは、完全なインタフェースのサブセットに従うかぎり、標準 C++ ライブラリなしで使用することができます。

11.10.1 移植のために標準 C++ ライブラリに留意する場合

制約付きのサブセットインタフェースは、それに対応する標準ライブラリベースのインタフェースと、ほとんど完全に上位互換性があります。主な違いは、コレクションがどの実装を使用しているかにより、コレクションの中には異なる数のテンプレートパラメータをとるものがあるということです。例えば、RWTPtrHashSet を標準 C++ ライブラリと共に使用する場合、これは前の節で説明されているように 3 つの引数をとります。ところが、同じクラスを標準 C++ ライブラリなしで使用する場合、制約付きインタフェースはただ 1 つのテンプレートパラメータ、すなわち含まれる項目の型を要求します。標準 C++ ライブラリの有無にかかわらず動作する移植可能なコードを書くために、Tools.h++ には次の 2 つのマクロが用意されています。

  1. 最初のマクロ RWDefHArgs(T) は Rogue Wave Default Hash Arguments の略で、ハッシュベースのテンプレートコレクションに使用します。例えば、以下のように宣言することにより、

    RWTPtrHashSet<int, RWDefHArgs(int)> hset;

    標準 C++ ライブラリの有無にかかわらず同じ意味論を持つハッシュベースのセットを宣言します。項目型とマクロの間には、コンマを使用してはいけないことに注意してください。標準 C++ ライブラリがない場合、マクロは何も拡張せず、あたかも以下のように宣言されたかのようになります。

    RWTPtrHashSet<int> hset;

    ところが、標準 C++ ライブラリが使用可能になると、マクロが以下のように拡張されます。

    RWTPtrHashSet<int ,RWTHasher<int>, equal_to<int> > hset;

    この宣言は、3 つのパラメータすべてにおいて標準ライブラリベースのインタフェースの必要条件を完全に満たし、別の標準ライブラリベースでない実装と一致するように意味論を保ちます。

  2. 2 番目のマクロ RWDefCArgs(T) は、最初のマクロと似ています。Rogue Wave Default Comparison Arguments の略である RWDefCArgs(T) は、RWTPtrSortedVector および RWTValSortedVector と共用するためにあります。例:

    RWTValSortedVector<int RWDefCArgs(int)> srtvec;

    は、標準 C++ ライブラリの有無にかかわらず動作する移植可能な宣言です。この場合も、項目の型をマクロと区切るためにコンマを使用しないでください。

11.10.2 例

標準 C++ ライブラリをもとにしていないクラスの 1 つである、RWTValVector<T> を使う簡単な例をみてみましょう。

#include <rw/tvvector.h>                             // 1

main() {
    RWTValVector<double> vec(20, 0.0);                     // 2

    int i;
    for (i=0; i<10; i++)     vec[i] = 1.0;                 // 3
    for (i=11; i<20; i++)    vec(i) = 2.0;                 // 4

    vec.reshape(30);                                           // 5
    for (i=21; i<30; i++)    vec[i] = 3.0;                 // 6
    return 0;
    }

以下にプログラムを行ごとに説明します。

//1
ここで、RWTValVector<T> のテンプレートが定義されています。
//2
20 項目長で 0.0 に初期化された倍精度のベクトルが宣言され、定義されています。
//3
ベクトルの最初の 10 個の項目が 1.0 に設定されます。ここでは、RWValVector<double>::operator[](int) が使用されています。この演算子は常に引数上で境界チェックを実行します。
//4
ベクトルの次の 10 個の項目が 2.0 に設定されます。この場合は、RWValVector<double>::operator()(int) が使用されました。この演算子は一般に境界チェックを実行しません。
//5
メンバ関数 reshape(int) はベクトルの長さを変更します。
//6
最終的に、最後の 10 個の項目が 3.0 に初期化されます。

11.10.3 例

2 番目の例では、ハッシュ辞書を使用します。ハッシュ辞書を宣言するときにマクロ RWDefHArgs(T) を使うことにより、標準 C++ ライブラリにアクセスできるかどうかにかかわらず、コードが間違いなく移植できるようになります。

#include <rw/tvhdict.h>
#include <rw/cstring.h>
#include <rw/rstream.h>
#include <iomanip.h>

class Count {                                                  // 1
  int  N;
public:
  Count() :    N(0) { }                                        // 2
  int  operator++()  { return ++N; }                           // 3
  operator   int() { return N; }                               // 4
};

unsigned hashString ( const RWCString& str )                   // 5
  { return str.hash(); }

main() {

  RWTValHashDictionary<RWCString,
                       Count  /* 注意: ここにはコンマを入れないこと! */
                       RWDefHArgs(RWCString)> hmap(hashString); //6

  RWCString token;
  while ( cin >> token )                                       // 7
    ++hmap[token];                                             // 8

  RWTValHashDictionaryIterator<RWCString,Count> next(hmap);    // 9

  cout.setf(ios::left, ios::adjustfield);                     // 10
  while ( ++next )                                            // 11
    cout << setw(20) << next.key()
         << " " << setw(10) << next.value() << endl;          // 12

  return 0;
}

入力:
How much wood could a woodchuck chuck if a woodchuck could chuck wood ?

出力:
much                 1
wood                 2
a                    2
if                   1
woodchuck            2
could                2
chuck                2
How                  1
?                    1

このコードでの問題は、入力ファイルを読み取り、それを空白類によって区切られたトークンに分解し、各トークンの個数を数えて、結果を出力することです。辞書を使って各トークンをそれぞれのカウンタにマップする方が、より一般的でしょう。次に、このコードを行ごとに説明します。

//1
これは、辞書の値の部分として使用されるクラスです。
//2
カウンタをゼロにするデフォルトコンストラクタが用意されます。
//3
前置インクリメント演算子を与えます。これは、便利な方法でカウンタをインクリメントするために使用されます。
//4
Count を int に変換する変換演算子が用意されます。これを使用して、結果を印刷します。または、多重定義の operator<<() を使用して Count にそれ自体を印刷する方法を指定することもできます。ただし、型変換演算子の方が簡単です。
//5
これは、辞書コンストラクタに指定しなければならない関数です。この関数の役目は、キーの型の引数で与えられたハッシュ値を返すことです。Tools.h++ では、RWCStringRWDateRWTimeRWWString の各クラスに、ユーザ指定の関数の代わりに使用できる静的 hash メンバ関数が用意されています。例を一般的にしておくために、Tools.h++ によって定義されている静的 hash メンバ関数のどれかではなく、ユーザ定義関数を使用することにしました。
//6

ここで辞書が生成されます。キーを指定すると、辞書を使って値を調べることができます。この場合は、キーは型 RWCString で、値は型 Count になります。コンストラクタには、唯一の引数である、キーを与えるとハッシュ値を返す関数へのポインタが必要です。この関数は、前記の // 5 で定義しました。RWDefHArgs(T) マクロを使って、標準 C++ ライブラリの有無にかかわらず、異なるプラットフォーム間でプログラムが確実に移植できるようにしました。

//7
トークンは、入力ストリームから RWCString に読み込まれます。これは EOF に到達するまで継続されます。これはどのように行われるのでしょうか? 式 cin >> token は単一トークンを読み取り、ostream& を返します。クラス ostream は void* への型変換演算子を持ち、while ループによって実際に条件判定されています。ストリームの状態が「正常」であれば、Operator void* は、this を返します。それ以外の状態の場合は、ゼロを返します。EOF によりストリームの状態が「正常」でなくなるため、EOF に到達すると while ループが終了します。『Tools.h++ Class Reference』でRWCStringの項目を、コンパイラに付属のクラスリファレンスガイドで「ios」の項目をそれぞれ参照してください。
//8

ここですべての重要な処理が行われます。オブジェクト map は辞書です。これは、多重定義されている operator[] を持ちます。この演算子はキーの型の引数をとり、それに対応する値への参照を返します。値の型は、Count であることを思い出してください。すなわち、map[token] は型 Count になります。// 3 でみたように、Count は多重定義されている前置インクリメント演算子を持ちます。これは Count 上で呼び出され、それによって値が増加します。

キーが辞書に無い場合はどうなるのでしょうか? 多重定義されている operator[] が、値のクラスのデフォルトコンストラクタを使って作成した新しい値とともにキーを挿入します。これは、カウンタをゼロに初期化するように // 2 で定義されています。

//9
結果を印刷するときがきました。辞書を一掃して各キーと値を返す、反復子を定義することにより開始します。
//10
見た目がきれいなように、出力ストリームのフィールド幅を調整します。
//11
反復子は、コレクションクラスの最後に到達するまで前進します。すべてのテンプレート反復子で、前置インクリメント演算子が反復子を前進し、それがコレクションクラスの最後を超えていないかどうかを調べます。
//12
反復子の位置にあるキーと値が印刷されます。

11.7 反復子

Tools.h++
には、コレクションクラス上で反復する方法がいくつか用意されています。ほとんどのコレクションで apply
メンバ関数が提供されています。この関数は、戻る前に指定された関数をコレクションの各項目に適用します。多くのコレクションに関連付けられている別個の反復子クラスによって、別の形式の反復が提供されています。例えば、RWTPtrDlistIterator<T>
を使って RWTPtrDlist<T>
の各項目を順にアクセスすることができます。反復子については、第 10 章「コレクションクラス」の
10.4 節 に説明があります。

11.7.1 標準 C++ ライブラリの反復子

Tools.h++
の標準ライブラリベースのコレクションクラステンプレートでは、標準反復子が提供されています。これらの反復子は、完全に標準
C++ ライブラリの反復子の必要条件に準拠しており、標準 C++
ライブラリ、特にそのアルゴリズムと共にクラスを使用する強力なツールです。反復子の完全な処理については、このマニュアルの目的から外れますので、標準
C++ ライブラリのリファレンスやチュートリアルを参照してください。

標準ライブラリベースのコレクションクラステンプレートには、前進、両方向、およびランダムアクセスの 3
種類の反復子があります。前進反復子は、最初から最後までの一方向の走査ができます。両方向反復子は、名前のとおり、最初から最後まで、および最後から最初までと、両方の方向に走査できます。ランダムアクセス反復子も両方向ですが、さらに一定時間に任意数の項目を走査する能力を持ちます。これらの反復子はすべて、間接参照演算子
* を使って現在の位置にある項目にアクセスすることができます。

反復子 iter と整数値 n
を使い、サポートされている基本的な演算のほんの一部を次に示します。

意味 対応方向
++iter; 次の項目に進み、戻る 前進、両方向、ランダム
iter++; 次の項目に進み、オリジナルの値を返す 前進、両方向、ランダム
*iter; 現在位置の項目への参照を返す 前進、両方向、ランダム
–iter; 前の項目を再処理し、戻る 両方向、ランダム
iter–; 前の項目を再処理しオリジナルの値を返す 両方向、ランダム
iter+=n; n 個の項目だけ進み、戻る ランダム
iter-=n; n 個の項目を再処理し、戻る ランダム

各型の反復子で使用できるすべての演算子と関数については、標準 C++
ライブラリのマニュアルを参照してください。

これらの反復子のほかにも、標準ライブラリベースのコレクションクラスには、コレクションクラスの項目上を反復するために使用する
iterator と const_iterator の 2 つの typedef
があります。iterator typedef
を使って、コレクションクラスを走査し、その中の項目を変更できます。const_iterator
のインスタンスを使って、コレクションクラスを走査して、項目にアクセスできますが、変更はできません。コレクションクラスにある項目を変更できない、連想コンテナベースおよびソート済みシーケンスベースのコレクションでは、iterator
と const_iterator の型は同じです。

最後に、テンプレートにも、対応するコレクションクラスを走査するために使用できる実際の反復子を返すメンバ関数が
2 つあります。. これらのメンバ関数は begin() と end()
です。これらのメンバ関数は、const でないバージョンは型 iterator
のインスタンスを返し、const バージョンは型 const_iterator
のインスタンスを返すように、それぞれ多重定義されています。

メンバ関数 begin()
は常にコレクションクラスの最初の項目にすでに置かれている反復子を返します。メンバ関数 end()
は、ヌルで区切られた文字列の NULL 文字へのポインタが past-the-end
値を持つように、past-the-end 値を持つ反復子を返します。past-the-end
値の反復子を使って、別の反復子と比較し、コレクションクラスの項目すべてにアクセスしたかを調べることができます。また、これは両方向またはランダムアクセス反復子のどちらかを持つコレクションクラス全体を、逆方向に移動する開始点として使うこともできます。end()
反復子では、それを間接参照することはできません。反復子を使って、リスト全体を移動し、一致するものを探す例を示します。

  RWTValDlist<int> intCollection; // 整数リスト

  // ... < リストに置く >

  // iter を開始点に配置する:
  RWTValDlist<int>::iterator iter = intCollection.begin();

  // 別の反復子を past-the-end に設定する:
  RWTValDlist<int>::iterator theEnd = intCollection.end();

  // 7 を探して全体を反復する:
  while (iter != theEnd) {       // コレクションの最後かどうかを調べる
  if (*iter == 7)              // 「*」を使って現在の項目にアクセスする
  return true;               // 7 が見つかった場合
  ++iter;                      // 7 でない場合は、次の項目に移動
  }
  return false;                  // まったく 7 が見つからなかった場合

11.7.2 Map ベースの反復とペア

RWMapVal<K,T,Compare>
のような map ベースのコレクションクラスの場合、反復子は標準 C++ ライブラリ構造
pair<const K, T> のインスタンスを参照します。map
ベースコレクション上を反復するとき、走査しながら各ステップでキーとそれに関連付けられたデータにアクセスします。pair
構造は、それぞれキーとそのデータに個別にアクセスできるメンバ first と second
を提供します。例:

  typedef RWTValMap<RWCString, RWDate, less<RWCString> > Datebook;

  Datebook birthdays;
  // ... < 誕生日コレクションを満たす >

  Datebook::iterator iter = birthdays.begin();

  while (iter != birthdays.end()) {
  cout << (*iter).first              // キー
  << " was born on "
  << (*iter).second             // そのキーのデータ
  << endl;

  ++iter;
  };

pair を non-const 参照しても、2
番目の項目であるキーに関連付けられているデータだけを変更できることに注意してください。これは、最初の項目は型
const K
として宣言されているからです。コレクションクラス内のオブジェクトの配置は、キーの値を基準にして内部的に保持されているので、キーを
const
として宣言することにより、コレクションクラスの統合性が保護されます。マップ内でキーを変更するためには、キーとそのデータを完全に削除し、それを新しいエントリで置き換える必要があります。

11.7.3 汎用化ポインタとしての反復子

最初ははっきりしないかもしれませんが、反復子を汎用化されたポインタとして考えることもできます。int
の配列へのポインタを想像してください。配列自体がコレクションクラスで、その配列の項目へのポインタはランダムアクセス反復子です。次の項目に進むためには、単項演算子
++ を使うだけです。前の項目に戻るには、–
を使います。反復子の現在位置にある項目にアクセスするには、単項演算子 *
を使用します。最後に、いつすべての項目にアクセスし終わったかを知ることが重要です。C++
は、割り当てられた配列の最後を過ぎた最初のアドレスを指すことができることを保証しています。例:

  int intCollection[10];         // 整数の配列

  // ... < 配列内に置く >

  // iter を開始点に配置する:
  int* iter = intCollection;

  // 別の反復子を past-the-end に設定する:
  int* theEnd = intCollection + 10;

  // 7 を探して全体を反復する:
  while (iter != theEnd) {       // 配列の最後かどうか調べる
  if (*iter == 7)              // '*' を使って現在の項目にアクセスする
  return true;               // 7 が見つかった場合
  ++iter;                      // 7 でない場合は、次の項目に移動
  }
  return false;                  // まったく 7 が見つからなかった場合

このコード例を 11.7.1.
にある標準反復子を使用した例と比較すると、よく似ていることがわかります。標準反復子がどのように働くか想像がつかない場合は、それらを汎用化されたポインタとして考えることができます。

 

11.6 ハッシングと等値の関数オブジェクト

連想ハッシュベースのテンプレートは、ハッシュ関数オブジェクトを使って、コレクションクラス内でオブジェクトをどのように位置付けし、見つけるかを決定します。ハッシュベースのコレクションを使うと、一定時間でオブジェクトの取り出しができ、効率が良いという利点があります。欠点は、コレクションクラス自体の物理的レイアウトにハッシュ関数の結果を割り当てることによって決定される順序で、オブジェクトが維持されることです。ハッシュベースコレクションの内部使用のほかには、この順序はめったに有用な意味を持つことはありません。

まったくの無秩序を避けるために、連想ハッシュベースのコレクションは等値オブジェクトを利用します。互いに等しい複数キーを許可するコレクションは、等値オブジェクトを使って、等値であるオブジェクトが一緒にグループ化されることを確実にします。複数キーを許可しないハッシュコレクションは、等値オブジェクトを使って、確実に一意の項目だけが許可されるようにします。これを行うためには、収集される項目の型に加えて、ハッシュ関数オブジェクトと等値オブジェクトの
2 つのテンプレート引数が必要です。

ハッシュ関数オブジェクトは、コレクションクラスの候補項目への const 参照を持ち、符号なしの
long ハッシュ値を返すような const 関数呼び出し演算子を含むクラスあるいは struct
です。等値オブジェクトは、オブジェクトのグループ化またはコレクションクラス内の一意性を確実にするために、コレクションクラスの
2 つの候補項目をとり、項目が等値とみなされる場合に true を返すような const
関数呼び出し演算子を含むクラス、または struct
です。ハッシュ関数オブジェクトと等値オブジェクトは、関係していなければなりません。つまり、等値とみなされるいかなる
2 つのオブジェクトもまた、同じハッシュ値を持たなければなりません。例:

  #include <rw/tvhset.h>   // RWTValHashSet を含む
  #include <rw/cstring.h>  // RWCString を含む

  struct StringHash {
  unsigned long operator()(const RWCString& s) const
  {  return s.hash(); }
  };

  struct StringEqual {
  RWBoolean operator()(const RWCString& s,
  const RWCString& t) const
  {  return s == t; }
  };

  RWTValHashSet<RWCString, StringHash, StringEqual> rwhset;

ここでは、型 RWCStringRWTValHashSet
を、独自のハッシュ関数オブジェクトと等値オブジェクトで具体化します。この例では、テンプレート引数として使用するために、これらの
struct を作成する方法が示されています (この場合、実際には equal-to
<RWCString> を等値オブジェクトに使用するかもしれませんが)。

 

15.3 まとめ

一般に、独自のクラスを設計するときに、これらの仮想関数すべてを定義する必要はありません。例えば、設計するクラスがソート済みコレクションを使うことはないと分かっている場合は、compareTo() を定義する必要はありません。しかし、いずれにしても仮想関数すべてを定義しておくことは大切です。コードの再使用を推進する上では、最良の方法です。

次に、クラス Bus の完全なコードを示します。

BUS.H:

#ifndef __BUS_H__
#define __BUS_H__

#include <rw/rwset.h>
#include <rw/collstr.h>

class Bus : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Bus)

public:

  Bus();
  Bus(int busno, const RWCString& driver);
  ~Bus();

  // クラス "RWCollectable" を継承:
  Rwspace      binaryStoreSize() const;
  int          compareTo(const RWCollectable*) const;
  RWBoolean    isEqual(const RWCollectable*) const;
  unsigned     hash() const;
  void         restoreGuts(RWFile&);
  void         restoreGuts(RWvistream&);
  void         saveGuts(RWFile&) const;
  void         saveGuts(RWvostream&) const;

  void         addPassenger(const char* name);
  void         addCustomer(const char* name);
  size_t       customers() const;
  size_t       passengers() const;
  RWCString    driver() const     {return driver_;}
  int          number() const     {return busNumber_;}

private:

  RWSet        customers_;
  RWSet*       passengers_;
  int          busNumber_;
  RWCString    driver_;
};

class Client : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Client)
  Client();
  Client(const char* name);
  Rwspace      binaryStoreSize() const;
  int          compareTo(const RWCollectable*) const;
  RWBoolean    isEqual(const RWCollectable*) const;
  unsigned     hash() const;
  void         restoreGuts(RWFile&);
  void         restoreGuts(RWvistream&);
  void         saveGuts(RWFile&) const;
  void         saveGuts(RWvostream&) const;
private:
  RWCString    name_;
//この例では他のクライアント情報を無視する
};
#endif

BUS.CPP:

#include "bus.h"
#include <rw/pstream.h>
#include <rw/rwfile.h>
#ifdef __GLOCK__
#  include <fstream.hxx>
#else
#  include <fstream.h>
#endif

RWDEFINE_COLLECTABLE(Bus, 200)

Bus::Bus() :
  busNumber_  (0),
  driver_     ("Unknown"),
  passengers_ (rwnil)
{}

Bus::Bus(int busno, const RWCString& driver) :
  busNumber_  (busno),
  driver_     (driver),
  passengers_ (rwnil)
{}

Bus::~Bus()  {
  customers_.clearAndDestroy();
  delete passengers_;
}

RWspace
Bus::binaryStoreSize() const  {
  RWspace count = RWCollectable::binaryStoreSize() +
    customers_.recursiveStoreSize() +
    sizeof(busNumber_) +
    driver_.binaryStoreSize();

 if (passengers_)
     count += passengers_->recursiveStoreSize();
   else
     count += RWCollectable::nilStoreSize();

   return count;
}

int
Bus::compareTo(const RWCollectable* c) const  {
  const Bus* b = (const Bus*)c;
  if (busNumber_ == b->busNumber_) return 0;
  return busNumber_ > b->busNumber_ ? 1 : -1;
}

RWBoolean
Bus::isEqual(const RWCollectable* c) const  {
  const Bus* b = (const Bus*)c;
  return busNumber_ == b->busNumber_;
}

unsigned
Bus::hash() const  {
  return (unsigned)busNumber_;
}

size_t
Bus::customers() const  {
  return customers_.entries();
}

size_t
Bus::passengers() const    return passengers_ ? passengers_->entries() :
0;
}

void
Bus::saveGuts(RWFile& f) const  {
  RWCollectable::saveGuts(f);    // 抽象基底クラスを保存する
 f.Write(busNumber_);           // プリミティブを直接書き込む
  f << driver_ << customers_;    // Rogue Wave が提供するバージョンを使用する
  f << passengers_;        // nil ポインタを自動的に検知する
}

void
Bus::saveGuts(RWvostream& strm) const  {
  RWCollectable::saveGuts(strm);   // 抽象基底クラスを保存する
  strm << busNumber_;              // プリミティブを直接書き込む
  strm << driver_ << customers_;   // Rogue Wave が提供する
                                   // バージョンを使用する
  strm << passengers_;     // nil ポインタを自動的に検知する
}

void Bus::restoreGuts(RWFile& f)  {
  RWCollectable::restoreGuts(f);  // 抽象基底クラスを復元する
  f.Read(busNumber_);             // プリミティブを復元する
  f >> driver_ >> customers_;     // Rogue Wave が提供する
                                  // バージョンを使用する

  delete passengers_;             // 古い RWSet を削除する
  f >> passengers_;               // 新しいもので置き換える
}

void Bus::restoreGuts(RWvistream& strm)  {
  RWCollectable::restoreGuts(strm); // 抽象基底クラスを復元する

  strm >> busNumber_ >> driver_ >> customers_;

  delete passengers_;             // 古い RWSet を削除する
  strm >> passengers_;            // 新しいもので置き換える
}

void
Bus::addPassenger(const char* name)  {
  Client* s = new Client(name);
  customers_.insert( s );

  if (!passengers_)
    passengers_ = new RWSet;

  passengers_->insert(s);
}

void
Bus::addCustomer(const char* name)  {
  customers_.insert( new Client(name) );
}

///////////////    Here are Client methods //////////////
RWDEFINE_NAMED_COLLECTABLE(Client,"client")

Client::Client() {} // RWCString デフォルトコンストラクタを使用する

Client::Client(const char* name) : name_(name) {}

RWspace
Client::binaryStoreSize() const {
  return name_->binaryStoreSize();
}

int
Client::compareTo(const RWCollectable* c) const  {
  return name_.compareTo(((Client*)c)->name_);
}

RWBoolean
Client::isEqual(const RWCollectable* c) const  {
  return name_ == *(Client*)c;
}

unsigned
Client::hash() const  {
  return name_.hash();
}

void
Client::restoreGuts(RWFile& f)  {
  f >> name_;
}

void
Client::restoreGuts(RWvistream& vis)  {
  vis >> name_;
}

void
Client::saveGuts(RWFile& f) const  {
  f << name_;
}

void
Client::saveGuts(RWvostream& vos) const  {
  vos << name_;
}

main()  {
  Bus theBus(1, "Kesey");
  theBus.addPassenger("Frank");
  theBus.addPassenger("Paula");
  theBus.addCustomer("Dan");
  theBus.addCustomer("Chris");

  { // ブロックがストリームの寿命を制御する
    ofstream f("bus.str");
    RWpostream stream(f);
    stream << theBus;      // theBus を ASCII ストリームに格納する
  }

  {
    ifstream f("bus.str");
    RWpistream stream(f);
    Bus* newBus;
    stream >> newBus;      // ASCII ストリームから復元する

    cout << "Bus number " << newBus->number()
         << " has been restored; its driver is "
         << newBus->driver() << ".\n";
    cout << "It has " << newBus->customers()
         << " customers and "
        << newBus->passengers() << " passengers.\n\n";

    delete newBus;
  }

  return 0;
}

出力:

Bus number 1 has been restored; its driver is Kesey.
It has 4 customers and 2 passengers.

15.2 RWCollectable オブジェクトの作成方法

次に、RWCollectable から派生したオブジェクトの作り方の概要を示します。詳しい情報については、各ステップごとに示されている節を参照してください。

  1. デフォルトコンストラクタを定義する 15.2.1 節を参照。
  2. マクロ RWDECLARE_COLLECTABLE をクラス宣言に追加する 15.2.2 節を参照。
  3. 2 つの定義マクロ RWDEFINE_COLLECTABLE、RWDEFINE_NAMED_COLLECTABLE のいずれかを、コンパイルするソースファイル (.cpp) の 1 つだけに追加して、作成するクラスのクラス ID を指定する 15.2.3 節を参照。
  4. 必要に応じて、継承した仮想関数の定義を追加する。継承した定義を使うことができる場合があります。以下の仮想関数については、15.2.4 節で説明されています。
    Int         compareTo(const RWCollectable*) const;
    RWBoolean   isEqual(const RWCollectable*) const;
    unsigned    hash() const;
    
  5. デストラクタを定義する必要があるかどうかを考慮する 15.2.5 節を参照。
  6. クラスに永続性を追加する。継承した定義を使うことができる場合、あるいは以下の関数の定義を追加しなければならない場合があります 15.2.6 節を参照。
    RWspace     binaryStoreSize() const;
    void        restoreGuts(RWFile&);
    void        restoreGuts(RWvistream&);
    void        saveGuts(RWFile&) const;
    void        saveGuts(RWvostream&) const;
    

RWFactory について」で、これらの手順が説明されています 15.2.7 節を参照。

15.2.1 デフォルトコンストラクタを定義する

すべての RWCollectable クラスには、デフォルトコンストラクタが必要です。デフォルトコンストラクタは引数をとりません。永続性機構は、このコンストラクタを使って空のオブジェクトを作成し、次に適切な内容でそのオブジェクトを復元します。

デフォルトコンストラクタは、C++ でオブジェクトのベクトルを作成するために必要です。そのため、デフォルトコンストラクタを定義することは、よい習慣だと言えます。次に、作成するクラス Bus のデフォルトコンストラクタの可能な定義を示します。

Bus::Bus() :
  busNumber_  (0),
  driver_     ("Unknown"),
  passengers_ (rwnil)
{
}

15.2.2 RWDECLARE_COLLECTABLE() をクラス宣言に追加する

15.1.1節にある例には、Bus の宣言にマクロの呼び出し RWDECLARE_COLLECTABLE(Bus) が含まれています。このマクロは、クラス名を引数にしてクラス宣言に記述しなければなりません。このマクロを使用することにより、必要なメンバ関数すべてが正しく宣言されます。

15.2.3 作成するクラスのクラス ID を指定する

多形永続性を使うと、クラスを 1 つの実行可能ファイルに保存し、それを別々の実行可能ファイルに復元したり、あるいはオリジナルの実行可能ファイルを別に実行する時に復元することができます。実行可能ファイルの復元時にそのクラスを使うことができ、その時にはクラスの型が既知である必要はありません。多形永続性を与えるためには、クラスに唯一の 不変の識別子 (ID) が必要です。[1] RWCollectable から派生したクラスは、多形永続性を持つため、そのような識別子を必要とします。

識別子は、数字でも文字列でも構いません。数字の識別子は、RWClassID で typedef された unsigned short になります。文字列の識別子は、RWStringID で typedef されます。数字の識別子を指定する場合は、クラスは自動的に生成された文字列識別子を持ちます。これは、クラス名と同じ順序の文字列です。同様に、文字列の識別子を指定する場合、実行可能ファイルとして使用されるとき、クラスは自動的に生成された数字の識別子を持ちます。

Tools.h++ には、ユーザが作成するクラスに識別子を与えるマクロが 2 つ含まれています。数字の識別子を指定する場合は、次を使用してください。

RWDEFINE_COLLECTABLE (className, numericID)

文字列の識別子を指定する場合は、次を使用してください。

RWDEFINE_NAMED_COLLECTABLE (className, stringID)

定義マクロは、クラスのヘッダファイルに含めないことに注意してください。マクロは、そのクラスを使用する .cpp ファイルの一部になります。作成する RWCollectable クラスごとに、定義マクロを 1 つだけ、ソースファイル (.cpp) の 1 つにだけ含める必要があります。クラス名を最初の引数として、数字または文字列のクラス識別子を 2 番目の引数として使用します。バスの例では、次の定義マクロを含めることができます。

RWDEFINE_COLLECTABLE(Bus, 200)

あるいは:

RWDEFINE_NAMED_COLLECTABLE(Client, "a client")

最初の例では、クラス Bus に数値 ID 200 を、2 番目の例では、クラス Client に文字列 ID “a client” を与えます。

今後、このマニュアルでは RWDEFINITION_MACRO を使って、このマクロのどちらでも選択できることを表わします。コード例では、いずれかのマクロが選択されています。

どちらのマクロも自動的に仮想関数 isA() と newSpecies() を定義します。[2] 15.2.3.1 節から15.2.7 節では、これらの仮想関数について解説すると共に、バージョン 7 の Tools.h++ で追加された stringID() メソッドについて説明し、多形永続性の実装を助けるクラス RWFactory についても簡単に触れることにします。

15.2.3.1仮想関数 isA( )

仮想関数 isA() はクラス ID を返します: オブジェクトのクラスを識別する唯一の数字であるクラス ID を返します。これを使って、オブジェクトが属するクラスを判断することができます。マクロ RWDECLARE_COLLECTABLE は、次のように関数宣言します。

virtual RWClassID isA() const;

RWClassID は、実際には unsigned short として typedef されています。0×8000 (hex) 以上の数値は、Rogue Wave の予約値です。数値のクラス ID は、0×0001 から 0x7fff の間で選択できます。Tools.h++ クラスライブラリには、<rw/tooldefs.h> で定義されているクラスシンボルセットがあります。一般に、これらのクラスシンボルは、2 つの下線に続く大文字のクラス名から構成されています。例:

RWCollectableString yogi;
yogi.isA() == __RWCOLLECTABLESTRING;           // Evaluates TRUE

マクロ RWDECLARE_COLLECTABLE(className) で、自動的に isA() が宣言されます。そして、いずれかの RWDEFINITION_MACRO で、定義されます。

15.2.3.2 仮想関数 newSpecies()

この関数の役目は、自分自身と同じ型の新しいオブジェクトを指すポインタを返すことです。マクロ RWDECLARE_COLLECTABLE は、次のように関数宣言します。

virtual RWCollectable*  newSpecies() const;

この関数は、いずれのバージョンの RWDEFINITION_MACRO によっても自動的に定義されます。

15.2.3.3 関数 stringID()

関数 stringID() は仮想関数のように動作しますが、実際は仮想関数ではありません。[3] この関数は、RWStringID のインスタンス、つまりオブジェクトのクラスを識別する一意の文字列を返します。RWStringID はクラス RWCString から派生しています。デフォルトで、クラスの文字列識別子は、クラスと同じ名前になります。RWStringID は、RWClassID の代わりに使用したり、RWClassID を捕捉するために使うことができます。

15.2.4仮想関数の定義を追加する

クラス RWCollectable では、次の仮想関数を宣言します。

virtual                 ~RWCollectable();
virtual Rwspace         binaryStoreSize() const;
virtual int             compareTo(const RWCollectable*) const;
virtual unsigned        hash() const;
virtual RWClassID       isA() const;
virtual RWBoolean       isEqual(const RWCollectable*) const;
virtual RWCollectable*  newSpecies() const;
virtual void            restoreGuts(RWvistream&);
virtual void            restoreGuts(RWFile&);
virtual void            saveGuts(RWvostream&) const;
virtual void            saveGuts(RWFile&) const;

これらの関数では、RWBoolean は int として、RWspace は unsigned long として、RWClassID は unsigned short としてそれぞれ typedef されています。クラス RWCollectable から派生したクラスはどれも、これらのメソッドを理解できます。基底クラス RWCollectable でこれらすべてにデフォルト定義が与えられていますが、クラスの設計者としてユーザがカスタマイズした定義を与えるのが最善です。

これらの仮想関数については、2 つに分けて説明を行います。15.2.5 節 ではデストラクタについて、15.2.6 節 では、クラスに永続性を追加する方法について説明すると共に、binaryStoreSize()、saveGuts()、restoreGuts() の各関数について説明します。仮想関数 isA() と newSpecies() は、マクロによって宣言され、定義されます。これについては、15.2.3.1 節15.2.3.2 節ですでに説明してあります。この節では、compareTo()、isEqual()、および hash() について説明しました。同じデータをこれら 3 つの関数がどのように処理するかを示した簡単な例が、15.2.4.4 節にあります。

15.2.4.1 仮想関数 compareTo()

仮想関数 compareTo() は、互いの比較関係によってオブジェクトを順序付けるのに使用します。この関数は、 RWBinaryTreeRWBTree などのように、順序付けに依存しているコレクションクラスで必要になります。これは、以下のように宣言されます。

virtual int  compareTo(const RWCollectable*) const;

関数 int compareTo(const RWCollectable*) const は、引数よりもそれ自身が大きい場合は正の値を、引数よりもそれ自身が小さい場合は負の値を、引数とそれ自体が等しい場合は、ゼロを返すように定義してください。

あるオブジェクトが別のオブジェクトより、大きい、小さい、あるいは等しいことの定義と意味は、クラスの設計者に任されています。 RWCollectable クラスにみられるように、デフォルトの定義では 2 つのオブジェクトのアドレスを比較するようになっています。このデフォルトの定義はプレースホルダとして考えてください。実際には、デフォルト定義は、プログラムを実行する度に変化する可能性があるので、あまり意味のあるものではありません。

次に、compareTo() の定義例を示します。

int Bus::compareTo(const RWCollectable* c) const
{  const Bus* b = (const Bus*)c;
   if (busNumber_ == b->busNumber_) return 0;
   return busNumber_ > b->busNumber_ ? 1 : -1;
}

ここでは、バスの順序付けの方法として、バスの番号を使用しています。複数のバスを RWBinaryTree に挿入する場合、それらはバス番号によってソートされます。この他にもいろいろな定義があることに注意してください。例えば、運転手の名前を使うこともできます。この場合、バスは運転手の名前によってソートされます。どのような定義をするかは、特定の問題により異なります。

ただし、ここには落とし穴があります。 RWCollectable の引数 c が指している実際の型は、常に Bus であるという仮定に基づいているのです。ユーザがうっかりコレクションに RWCollectableString を挿入したりすると、キャスト (const Bus*)c は、無効なものになり、それを間接参照すると予期しない結果を招くことになります。[4] 多重定義された仮想関数は同じシグニチャを共有しなければならないため、この場合は、最小共有分母であるクラス RWCollectable にならざる得ないのです。つまり、コンパイル時の型検査はできません。

コレクションのメンバは同種 (すべて同じ型) であるか、あるいはそれらの型を区別する方法が存在しなければなりません。型の識別には、メンバ関数 isA() またはstringID() を使用できます。

15.2.4.2 仮想関数 Equal()

仮想関数 isEqual() は、12.3.1 節で説明した汎用コレクションのテスタ関数と同じような働きをします。

RWBoolean  isEqual(const RWCollectable* c) const;

関数 RWBoolean isEqual(const RWCollectable*) は、オブジェクトと引数が等値であるとみなされた場合は TRUE を、そうでなければ FALSE を返すように定義します。等値性の定義は、クラスの設計者に任されています。クラス RWCollectable で定義されているように、デフォルトの定義では 2 つのアドレスが等しいときに「等しい」とみなされます。すなわち、これはアイデンティティの同定と同じです。

2 つのオブジェクトが同一であることを「等しい」とみなすのであれば、isEqual を再定義する必要はありません。しかし、isEqual は、2 つのオブジェクトが別の条件で等しいことを意味することも可能です。また、2 つのオブジェクトは同じ型である必要がありません。唯一の条件は、引数として渡されるオブジェクトが型 RWCollectable を継承していることです。責任を持って、型キャストが適切に行われるようにしてください。

比較した結果等しい 2 つのオブジェクト (compareTo() がゼロを返す) で、isEqual() が TRUE を返さなければならないという規則はありません。ただし、そのような例はあまり存在しません。また、異なるハッシュ値を持つオブジェクトが、isEqual で TRUE を返すようなクラスを設計することも可能です。ただし、このようなオブジェクトは、ハッシュベースのコレクションで検索することが不可能になります。

Bus クラスの場合、isEqual の適切な定義は、以下のようになります。

RWBoolean Bus::isEqual(const RWCollectable* c) const
{  const Bus* b = (const Bus*)c;
   return busNumber_ == b->busNumber_;
}

ここでは、バス番号が同じ場合に、バスは等しいとみなされます。前にも述べたように、これ以外の定義も可能です。

15.2.4.3 仮想関数 hash()

関数 hash() は、オブジェクトに対して適切なハッシュ値を返すように定義します。ここでは、次のように宣言します。

unsigned  hash() const;

Bus クラスの hash() は、次のように定義することができます。

unsigned Bus::hash() const{
   return (unsigned)busNumber_;
}

上の例では、ハッシュ値として単純にバス番号を返します。以下のように、ハッシュ値としてドライバ名を使うこともできます。

unsigned Bus::hash() const{
   return driver_.hash();
}

上記例では、driver_ は、すでに定義されたハッシュ関数を持つ RWCString です。

アイデンティティ: 2 つのオブジェクトが isEqual で TRUE を返す場合は、ハッシュ値も同じになります。

15.2.4.4 compareTo()、isEqual()、および hash() の例

これまで、3 つの派生仮想関数、compareTo()、isEqual()、および hash() について説明しました。次に一組のオブジェクトを定義し、関数を適用する例を示します。関数の結果は、コードのコメントとして記載されています。

RWCollectableString a("a");
RWCollectableString b("b");
RWCollectableString a2("a");

a.compareTo(&b);      // -1 を返す
a.compareTo(&a2);     // 0 を返す (等しいとみなされる)
b.compareTo(&a);      // 1 を返す

a.isEqual(&a2);       // TRUE を返す
a.isEqual(&b);        // FALSE を返す

a.hash()              // 96 を返す (オペレーティングシステムにより異なる)

RWCollectableString の関数 compareTo() は、文字列を辞書的順序で大文字小文字の区別付きで比較するように定義されていることに注意してください。RWCString について詳しくは、『Tools.h++ Class Reference』を参照して ください。

15.2.5 オブジェクトの消滅

RWCollectable クラスを継承するオブジェクトはすべて仮想デストラクタを継承します。つまり、オブジェクトを削除するとき、そのオブジェクトの実際の型を知る必要がありません。これにより、コレクション内の項目はすべて、その実際の型が分からなくても削除されます。

C++ のクラスと同様に、RWCollectable を継承しているオブジェクトは、保持しているリソースを解放するためにデストラクタが必要です。 Bus の場合、乗客と利用者の名前は、ヒープ領域から割り当てられた RWCollectableString です。つまり、それらは返却しなければなりません。これらの文字列は、クラスの外からは見えないため、ユーザがそれらにアクセスすることを心配する必要はありません。つまり、これらの文字列は、ぶら下がりポインタが残る心配をせずに、デストラクタで削除できます。

さらに、passengers_ が指す群は customers_ が指す群に含まれているので、customers_ の内容だけを削除すればいいことになります。

デストラクタは次のように定義することができます。

Bus::~Bus()
{  customers_.clearAndDestroy();
   delete passengers_;
}

ポインタ passengers_ が nil ポインタであっても、delete を呼び出しても構わないことが、C++ 言語で保証されています。

15.2.6 多形永続性を追加する方法

仮想関数 saveGuts() と restoreGuts() には、RWCollectabl e オブジェクトの内部状態を保存および復元する責任があります。 RWCollectable クラスに永続性を追加するには、オブジェクトのメンバデータを書き出すように、仮想メンバ関数 saveGuts() と restoreGuts() をオーバーライドしなければなりません。15.2.6.1 節15.2.6.2 節では、これらの関数を正しく定義する方法を説明します。15.2.6.3 節では、これらの関数がどのように多重参照されているオブジェクトを処理するかを説明します。

オブジェクトを多形的にファイルに保存するためには、オブジェクトの格納に割り当てる必要があるバイト数を知らなければなりません。この値は、binaryStoreSize() 関数によって計算されます。binaryStoreSize() の使い方は、15.2.6.4 節を参照してください。

RWCollection には、そのクラスを継承するコレクションを多形的に保存するのに使用できる、独自のバージョンの saveGuts() と restoreGuts() 関数があります。これらの関数がどのように機能するかは、15.2.6.5節で簡単に説明されています。

15.2.6.1 仮想関数 saveGuts(RWFile&) と saveGuts(RWvostream&)

仮想関数 saveGuts(RWFile&) と saveGuts(RWvostream&) の役割は、RWCollectable オブジェクトの内部状態を、クラス RWFile を使ってバイナリファイルに、あるいはクラス Rwvostream を使って仮想出力ストリームに多形的に保存することです。これにより、その後、あるいは別の場所でオブジェクトを復元することができます。次に、saveGuts() 関数を定義するときの規則を示します。

  1. 基底クラスのバージョンの saveGuts() を呼び出すことによって基底クラスの状態を保存します。
  2. メンバデータの型ごとに、その状態を保存します。関数をどのように作成するかは、メンバデータの型によります。
    • プリミティブ- プリミティブの場合は、データを直接保存します。 RWFile に保存する場合は、RWFile::Write() を使います。仮想ストリームに保存する場合は、挿入演算子 RWvostream::operator<<() を使います。
    • Rogue Wave のクラス- ほとんどの Rogue Wave クラスには、多重定義されたバージョンの挿入演算子があります。例えば、RWCString の場合は以下のとおりです。
      RWvostream& operator<<(RWvostream&, const RWCString& str);
      

      つまり、Rogue Wave の多くのクラスでは、単にストリームに移動できます。

    • WCollectable を継承するオブジェクト- これらのオブジェクトのほとんどで、グローバル関数を使います。
      RWvostream& operator<<(RWvostream&,
                             const RWCollectable& obj);
      

      この関数は、オブジェクトに対して saveGuts() を再帰的に呼び出します。

これらの規則に留意した上で、例 Bus の関数 saveGuts() の定義をみてみましょう。

void Bus::saveGuts(RWFile& f) const
{  RWCollectable::saveGuts(f);    // 抽象基底クラスを保存する
   f.Write(busNumber_);           // プリミティブを直接書き込む
   f << driver_ << customers_;    // Rogue Wave が提供する
                                  //バージョンを使用する
   f << passengers_;              // nil ポインタを自動的に
                                  // 削除する
}

void Bus::saveGuts(RWvostream& strm) const
{  RWCollectable::saveGuts(strm); // 抽象基底クラスを保存する
   strm << busNumber_;            // プリミティブを直接書き込む
   strm << driver_ << customers_; // Rogue Wave が提供する
                                  // バージョンを使用する
   strm << passengers_;           // nil ポインタを自動的に
                                  // 削除する
}

メンバデータ busNumber_ は、整数型で C++ のプリミティブです。これは、RWFile::Write(int) または RWvostream::operator<< (int) のいずれかを用いて、直接格納されます。

メンバデータ driver_ は RWCString です。これは、RWCollectable を継承せず、次のようにして格納されます。

RWvostream& operator<<(RWvostream&, const RWCString&);

メンバデータ customers_ は RWSet です。これは、RWCollectable を継承せず、次のようにして格納されます。

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

最後のメンバデータ passengers_ の格納には、コツが要ります。このデータは、RWCollectable を継承する RWSet へのポインタです。ただし、ポインタが nil である可能性があります。ポインタが nil の場合は、それを次のように渡します。

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

passengers_ を間接参照しなければならないので、とんでもないことになるかもしれません。

strm << *passengers_;

その代わりに、passenger_ は RWSet * として宣言されているので、以下のように渡します。

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

これは、自動的に nil ポインタを検知して、その記録を格納します。

15.2.6.2 仮想関数 restoreGuts(RWFile&) と restoreGuts(RWvistream&)

saveGuts() と同様の方法で、これらの仮想関数は、ファイルまたはストリームから RWCollectable の内部状態を復元するために使用されます。Bus クラスのこれらの関数は、次のように定義されています。

void Bus::restoreGuts(RWFile& f)
{  RWCollectable::restoreGuts(f);   // 抽象基底クラスを復元する
   f.Read(busNumber_);              // プリミティブを復元する
   f >> driver_ >> customers_;      // Rogue Wave が提供する
                                    // バージョンを使う

   delete passengers_;              // 古い RWSet を削除する
   f >> passengers_;                // 新しいもので置き換える
}

void Bus::restoreGuts(RWvistream& strm)
{  RWCollectable::restoreGuts(strm);   // 抽象基底クラスを復元する
   strm >> busNumber_ >> driver_ >> customers_;

   delete passengers_;                 // 古い RWSet を削除する
   strm >> passengers_;                // 新しいもので置き換える
}

ポインタ passengers_ が以下のように復元されることに注意してください。

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

オリジナルの passengers_ が nil ポインタでない場合、この関数は新しい RWSet をヒープ領域から復元し、それを指すポインタを返します。それ以外の場合は、nil ポインタを返します。いずれの場合も、passengers_ の内容が置き換えられます。つまり、まず delete passengers_ を呼び出さなければならないということです。

15.2.6.3 多重参照されているオブジェクト

乗客名は、customers_ が指している群と passengers_ が指している群の両方に存在できます。つまり、両方のコレクションが同じ文字列を含んでいます。Bus を復元するとき、ポインタ関係が保持されること、および復元されたものが文字列の別のコピーを作成しないようにしなければなりません。

ポインタ関係がそのまま保持されるためには、何も特別な操作をする必要がありません。次の例について考えてみてください。

Bus aBus;
RWFile aFile("busdata.dat");

aBus.addPassenger("John");
aFile << aBus;

passenger_ は customer_ のサブセットであるため、関数 addPassenger は、利用者リストと乗客リストの両方に名前を載せます。aBus を aFile に保存するとき、両方のリストが 1 つの呼び出しで保存されます。まず、利用者リストが、次に乗客リストが保存されます。多形永続性の機構では John への最初の参照は保存されますが、2 回目の参照は、単に最初のコピーへの参照が格納されるだけです。格納の間、コレクションの元の参照関係を複写して、同じオブジェクトを指す両方の参照が解決されます。

15.2.6.4 仮想関数 binaryStoreSize()

仮想関数 binaryStoreSize() は、RWFile を用いてオブジェクトを格納するのに必要なバイト数を計算します。これは、以下のように宣言されます。

virtual Rwspace  binaryStoreSize() const;

この関数は、格納する前にオブジェクトに領域を割り当てる必要がある、クラス RWFileManagerRWBTreeOnDisk の場合に便利です。実際に格納されたバイト数は、非仮想関数 recursiveStoreSize() によって返されます。再帰的格納サイズには、binaryStoreSize() が使用されます.

独自の binaryStoreSize() を作成することは、通常難しいことではありません。saveGuts(RWFile&) によって設定されているパターンに従い、メンバデータを保存するかわりに、そのサイズを加算します。ただし、構文的な違いが 1 つあります。挿入演算子の代わりに、sizeof() と以下に示すメンバ関数を使います。

  • プリミティブの場合は、sizeof() を使います。
  • RWCollectable を継承するオブジェクトの場合、ポインタが nil でなければ、次のメンバ関数を使います。
    RWspace RWCollectable::recursiveStoreSize();
    
  • RWCollectable を継承するオブジェクトの場合、ポインタが nil であれば、次のメンバ関数を使います。
    RWspace RWCollectable::nilStoreSize();
    
  • その他のオブジェクトの場合は、メンバ関数 binaryStoreSize() を使います。

次に、クラス Bus の binaryStoreSize() 関数の定義例を示します。

RWspace Bus::binaryStoreSize() const{
   RWspace count = RWCollectable::binaryStoreSize() +
     customers_.recursiveStoreSize() +
     sizeof(busNumber_) +
     driver_.binaryStoreSize();

   if (passengers_)
     count += passengers_->recursiveStoreSize();
   else
     count += RWCollectable::nilStoreSize();

   return count;
}

15.2.6.5 カスタムコレクションに多形永続性を持たせる

ほとんどのコレクションクラスには、クラス RWCollection に組み込まれているバージョンの saveGuts() と restoreGuts() で十分です。関数 RWCollection::saveGuts() は、コレクションの各項目ごとに、以下を繰り返し呼び出すことによって機能します。

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

同様に、RWCollection::restoreGuts() は、以下を繰り返し呼び出すことによって機能します。

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

この演算子は、ヒープ領域を適切な型の新しいオブジェクトに割り当てて、insert() を呼び出します。Rogue Wave の Smalltalk_likeコレクションクラスは RWCollection を継承しているので、すべてこの機構を使用します。

RWCollection クラスを継承する独自のコレクションクラスを作成する場合でも、独自の saveGuts() や restoreGuts() を定義する必要はめったにありません。

ただし、これには例外があります。例えば、クラス RWBinaryTree は独自のバージョンの saveGuts() を持っています。デフォルトバージョンの saveGuts() は項目を順に格納するために、これが必要となります。バイナリツリーの場合、これは、ツリーを読み戻すときに極端にアンバランスなツリーになる結果を招きます (特に、リンクリストの場合)。そのため、RWBinaryTree のバージョンの saveGuts() は、ツリーのレベルごとに格納を行います。

独自のクラスを設計するときは、同様に saveGuts() と restoreGuts() をカスタマイズしなければならない特殊な必要性があるかどうか、検討しなければなりません。

15.2.7 RWFactory について

ここでは、RWDEFINITION_MACRO について検討します。

RWDEFINE_COLLECTABLE(className, numericID)

文字列 ID を使う場合は、

RWDEFINE_NAMED_COLLECTABLE(className, stringID)

bus の例の .cpp ファイルでは、マクロは以下のように使われています。

RWDEFINE_COLLECTABLE(Bus, 200)

そして

RWDEFINE_NAMED_COLLECTABLE(Client, "a client")

これらのマクロを使用すると、RWClassID だけを与えることによって、プログラムで独自のクラスの新しいインスタンスを作成できます。

Bus* newBus = (Bus*)theFactory->create(200);

または RWStringID:

Client* aClient = (Client*)theFactory->create("a client");

ポインタ theFactory は、クラス RWFactory の唯一のグローバルインスタンスを指すグローバルポインタです。これは、実行可能ファイルにインスタンスを持つ RWCollectable クラスすべての情報を保持します。RWFactory の create() メソッドは、実行時に型不明の永続性を持つオブジェクトに新しいインスタンスを作成するために、多形永続性機構によって内部的に使用されています。通常、独自のソースコードでこの機能を使うことはありません。というのは、RWFactory の使用は一般に、ユーザにとって明白だからです。RWFactory について詳しくは、『Tools.h++ Class Reference』を参照してください。

RWFactory は実際には特殊化された RWSet で、ハッシュ表を用いて実装されています。これは、RW_DEFAULT_CAPACITY バケット (通常 64) で作成されています。プログラムが RWCollectable から派生した別個のクラスを 64 個以上を使用する場合は、以下のようなコードを使ってバケット数を増加してください。

RWFactory *fac = getRWFactory();
fac->resize(resize-Factor * fac->buckets()); // 大きなサイズを
                                             // 選ぶ

この演算には、項目数に比例して時間がかかります。サイズを増加することが分かってい
る場合は、できるだけ早めに行うのが最も効果的です。

注釈

  1. ^ 識別子は、どの実行可能ファイルの識別子とも異なる必要があります。
  2. ^ RWDEFINITION_MACRO マクロは、この 2 つのメソッド以上のものを実装するので、 提供されているマクロを使用しないことにする場合も、マクロの詳細を検討し、何が行われているかをよく理解することが大切です。
  3. ^ RWStringIDと仮想関数をどのようにして模擬するかについては、 18.3.3.2 節を参照してください。前のバージョンの Tools.h++ でコンパイルされたオブジェクトコードのリンク互換性を保持するために、コードがこのように書かれています。
  4. ^ これは、ユーザが常に認識しなければならない C++ の欠陥です。ユーザが異種コレクションを使う場合には、特に注意が必要です。この問題について詳しくは、第 14 章「永続性」の「異種の RWCollectable を格納するために RWCollection をソートしないこと」を参照。
  5. ^ 永続性の機構については、第 14 章「永続性」を参照

15.1 なぜ RWCollectable クラスを設計するのか?

RWCollectable クラスの設計方法の詳細に入る前に、あえて RWCollectable クラスを設計する理由を具体例で説明します。

例えば、バス会社を経営していると仮定しましょう。乗客記録システムの一部を自動化するために、バス、バスの利用者、実際の乗客を表わすような各クラスを作成したいとします。乗客になるためには、その人はバスの利用者でなければなりません。そのため、利用者群は、乗客群のスーパーセットになります。また、乗客は同時にバスに一度しか乗車することができないので、どの時点でも利用者リストに同じ人を 2 度以上載せることはできません。このシステムの開発者として、どちらのリストにも重複が許されないことを確実にしなければなりません。

これらの重複は問題を引き起こす可能性があります。このプログラムはバスとその利用者についての情報を保存し、それを復元できなければなりません。バスを多形的に保存するとき、プログラムが各人を保存しながら、単純に利用者群を一巡し、それから乗客群を一巡すると、利用者であると同時に乗客である者は 2 回保存されてしまいます。プログラムが多形的にバスを復元するとき、乗客のリストは利用者のリストにすでに載っている人を参照しなくなります。その代わり、各乗客は両方のリストに別々のインスタンスを持つことになります。

ある人がすでに多形的にストリームに保存されているとき、それを認識し、その人を再び保存するのではなく、単に前のインスタンスへの参照を保存するような方法が必要になります。

これがクラス RWCollectable の役目です。RWCollectable を継承するオブジェクトは、オブジェクトの内容だけでなく、RWCollectable を継承する他のオブジェクトとの関係も保存する能力を備えています。この機能を同形永続性と呼びます。RWCollectable クラスは同形永続性を持ちますが、それ以上の機能があります。つまり、実行時に保存されるあるいは復元されるオブジェクトの型を判断できます。RWCollectable によって提供されるこの種類の永続性を多形永続性と呼びます。

15.1.1 RWCollectable クラスの例

次のコードは、前の節で説明したクラスをどのように宣言するかを示しています。後でマクロ RWDECLARE_COLLECTABLE を使って、関数の選択について説明します。完全なコードは、この章の終わりに挙げられた例を見てください。また、toolexam ディレクトリにあるバスの例としても挙げられています。

class Bus : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Bus)

public:

   Bus();
   Bus(int busno, const RWCString& driver);
   ~Bus();

   // Inherited from class "RWCollectable":
   Rwspace     binaryStoreSize() const;
   int         compareTo(const RWCollectable*) const;
   RWBoolean   isEqual(const RWCollectable*) const;
   unsigned    hash() const;
   void        restoreGuts(RWFile&);
   void        restoreGuts(RWvistream&);
   void        saveGuts(RWFile&) const;
   void        saveGuts(RWvostream&) const;

   void        addPassenger(const char* name);
   void        addCustomer(const char* name);
   size_t      customers() const;
   size_t      passengers() const;
   RWCString   driver() const    {return driver_;}
   int         number() const    {return busNumber_;}

private:

   RWSet       customers_;
   RWSet*      passengers_;
   int         busNumber_;
   RWCString   driver_;
};

class Client : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Client)
  Client(const char* name) : name_(name) {}
private:
  RWCString    name_;
//ignore other client information for this example
};

両方のクラスが RWCollectable から派生していることに注意してください。重複項目を許可しないクラス RWSet を用いて、利用者群を実装することにしました。これにより、同じ人が利用者リストに 2 度以上入力されないことが保証されます。同じ理由で、RWSet クラスを用いて、乗客群を実装することにしました。ただし、こちらの乗客群はヒープ領域に設定することにしました。これについては、後の節で説明します。

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 をソートできません。例えば、RWCollectableStringRWCollectableDate を同一の RWCollection に格納する予定の場合、RWBtree などのソート済みの RWCollection コレクションに格納できないことを意味します。ソート済みの RWCollection には、RWBinaryTreeRWBtreeRWBTreeDictionary、および 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 が必要とする適切なコードをリンクするために必要な情報がリンカに与えられます。