[現在の実装はこう なっています。ここに記述されたものと違う場合があります。]
第二のケースでは継承クラスは親クラスの識別はできるでしょうが、一般に 親クラスのオブジェクトが子クラスのオブジェクトにキャスト出来ないように 正しいオブジェクトになりうるかどうか考える必要があります。
第三のケースでは新しいバージョンは古いバージョンのことを知っています。 そのオブジェクト記録は新しいバージョンに読み込まれたら、そのデータが 古いバージョンによることを警告する必要があるかも知れません。
最後の場合、古いバージョンは新しいバージョンのことを知りません。該当 するメンバーがあってその型が変わったときは記述可能でしょうが、追加 されたメンバーがあるときはどうしようもありません。この場合はエラーと して通知される必要があるかも知れません。
次に、一つのアプリケーションはファイルなどのやり取りをするとき、その アプリケーションが必要とするクラスのオブジェクトについてのみ興味があり、 任意のクラスのオブジェクトを読む必要はありません。すべての クラスの読み書きが出来ることは要求されません。クラスをカテゴリーに 区分したとき、そのアプリケーションが必要とするカテゴリーの数はそれほど 多くないはずです。KONOEではカテゴリー単位でクラスオブジェクトの生成を 管理します。カテゴリーマネージャを導入します。
メンバーデータには整数や実数、文字列のようなプリミティブとクラス オブジェクトの場合があります。オブジェクトがメンバーデータとして ローカルに保持されている場合はそのコピーをストリームにおとしたり、 ストリームから読み込んでオブジェクトを生成します。
注意が必要なのはポインターメンバーです。ポインターメンバーはその 使われ方によって処理を考える必要があります。
ここでは次のようなレコードフォーマットを提案します。
記録深度 | カテゴリー識別子 | クラス識別子 | クラスバージョン識別子 |
記録深度は入れ子になった構造体記録の深さを示すもので、最も外側の ものを'A'(0x41)とします。内側に向かって'B'、'C'と増えていきます。 このため、ファイルの場合最初のバイトは'A'でなければなりません。 これにより、KONOE記録ファイルであるかどうか、バイトスワップが必要か どうかを表します。
カテゴリー識別子はクラスカテゴリーを示すデータで、システム全体で ユニークでなければなりません。
カテゴリー識別子は8ビットであらわされ、それぞれクラスカテゴリーを 特定します。特殊なものとして、0はその記録がプリミティブ型のもので あること、1はオブジェクト配列であることを表します。
クラス識別子はその記録がどのクラスのオブジェクトであるかを与えます。 記録の中からクラスの継承関係を読み取るために、また、メンバーが どの継承段階で定義されたものかを明示するために、親クラスのデータは 継承クラスの中に入れ子になって記録されるようにします。そのためには バージョン識別子の一部を使って継承を表します。
クラス識別子はそのクラスカテゴリーの中で固有の値を持ちます。 プリミティブ型の場合は
バージョン識別子はクラスバージョンを識別します。プリミティブでも 意味がある場合がありえます。8ビットが割り当てられていますが、最上位 ビットは継承であり保持ではないことを示すフラッグとして使用します。
メンバー識別子 | 記録要素数 |
メンバーの識別は8ビットで行なわれます。 これは最大256までのメンバーしか記録できないことを示します。
記録要素数は下24ビットで表されます。最大1600万までしか配列を 記述できないことを示します。オブジェクト記録の場合この部分は メンバーの数になります。
レコードサイズ |
レコードサイズはそのレコード(記録)の長さのうち、ヘッダーワードを 除いた32ビットワード数を表します。この意味は バッファ上で、レコードサイズワードの次をさしているポインターに この長さを加えると次のヘッダーワードをさすことになるようになって いるということです。
実際のデータ |
プリミティブの場合は直接データが書かれます。クラスオブジェクトの場合 最初のメンバーのヘッダーがここに来ます。可変長配列の場合最初の オブジェクトのヘッダーがここに来ます。
オブジェクトポインターの場合、何らかの機構が、オブジェクトを識別する タグを認識し、該当するオブジェクトへのC++ポインターを返してくれ なければなりません。カテゴリー、クラス、オブジェクトIDの情報から 判定します。また、これらの番号はさまざまなところでコンシステントに 管理される必要があります。
class A { int i; double d[2]; };実際にはこのクラスAはカテゴリー2の3番目のクラスとすると、
'A' | 2 | 3 | 0 | 深さ1カテゴリー2クラス3バージョン0 | |
0 | 1 | メンバー0要素数2 | |||
11 | レコードサイズ | ||||
'B' | 0 | 1 | 0 | 深さ2プリミティブ型0整数型1バージョン0 | |
0 | 1 | メンバー0要素数1 | |||
1 | レコードサイズ1 | ||||
1234 | 実際の整数型データ | ||||
'B' | 0 | 5 | 0 | 深さ2プリミティブ型0倍精度5バージョン0 | |
1 | 2 | メンバー1要素数2 | |||
4 | レコードサイズ4 | ||||
XXXX | 最初の要素の上半分 | ||||
XXXX | 最初の要素の下半分 | ||||
YYYY | 二番目の要素の上半分 | ||||
YYYY | 二番目の要素の下半分 |
ここで一番外側のメンバー識別は意味がありませんので、別の用途に使えるで しょう。クラスメンバーにクラスオブジェクトがある場合は'B'の次が'L'では なくて'1'となり、そのメンバーは深さ'C'で続いていくことになります。
次に、オブジェクトのサイズを計算したり、個別のメンバーデータと バッファの間のコピーを行なうのはそれぞれのオブジェクトでなければ なりません。どのようなメンバーがあるかを知っているのはそのオブジェクト だけだからです。それゆえ、そのオブジェクトはストリーマブルクラスの 継承クラスでなければなりません。
KonoeObjectStreamBase & os; KonoeStreamableObject * op; os << * op;逆の場合はどうでしょうか。どのようなオブジェクトが取り出されるかは 読んでみないとわかりません。ヘッダーブロックを見て、そのクラス識別子 からオブジェクトを作るクラスを理解しなければなりません。
class KonoeObjectStreamBase { public: int put( KonoeStreamableObject * op ); // オブジェクトを書き出す。 int test( ); // 次のオブジェクトのヘッダーをテストする。 int getCategory( ); // カテゴリー識別子を読む。 int getClassID( ); // クラス識別子を読む。 };このようにオブジェクトストリームでは入力と出力の動作がかなり変わってきます。 その意味では、KonoeObjectStreamBaseをさらに継承して入力と出力をそれぞれ 用意することに意味があります。
class KonoeStreamableObject { protected: static const int category_id; static const int class_id; int object_id; public: static int match( int categoryId, int classId ); // 一致を確認 virtual int put( KonoeOutputObjectStream & os ); // ストリームへの書き出し。 virtual KonoeStreamableObject * get( KonoeInputObjectStream & os ); // ストリームからの読み出し };
このための方法として、すべてのクラスについて知っているサービス クラスを用意する手もありますが、あまり効率的とは思えません。(本来の オブジェクト指向システムではこのことが実装されていなければなりませんが。) そこで、これまでの議論でも想像できるように、カテゴリー毎にサービス クラスを用意することにします。