[C1.1]オブジェクトストリーム

ここでは、オブジェクトストリームの実装について詳しく検討していきましょう。 まず、オブジェクトはファイルやソケットを対象に入出力がなされることに なりますが、ファイルやソケット自身は単純なバイト列を扱うだけです。その ため、送り側と受け側が同じルールで読み書きを行なう必要があります。ここ では、まずそのルールを確立する必要があります。

[現在の実装はこう なっています。ここに記述されたものと違う場合があります。]

1.オブジェクトの属するクラスの識別

まず、単純なバイトストリームからオブジェクトを読み取る必要から、それが なんというクラスのオブジェクトであるか、クラスを識別できなければなりません。 そのクラスは読み手と書き手で一致していることが望ましいですが、次のような ケースを考える必要があります。
  1. 継承されたクラスのオブジェクトとして書かれ、親のクラスのオブジェクト として読まれる。
  2. 親のクラスのオブジェクトとして書かれ、継承クラスのオブジェクトとして 読まれる。
  3. 同じ名前のクラスであるけれども古いバージョンの実装によって 書かれ、新しいバージョンによって読まれる。
  4. 同じ名前のクラスであるけれども新しいバージョンによって書かれ、 古いバージョンによって読まれる。
最初のケースでは、親のクラスは自分の継承クラスであることを知る必要が あります。オブジェクトデータの中に継承関係が記述される必要があることを 示しています。

第二のケースでは継承クラスは親クラスの識別はできるでしょうが、一般に 親クラスのオブジェクトが子クラスのオブジェクトにキャスト出来ないように 正しいオブジェクトになりうるかどうか考える必要があります。

第三のケースでは新しいバージョンは古いバージョンのことを知っています。 そのオブジェクト記録は新しいバージョンに読み込まれたら、そのデータが 古いバージョンによることを警告する必要があるかも知れません。

最後の場合、古いバージョンは新しいバージョンのことを知りません。該当 するメンバーがあってその型が変わったときは記述可能でしょうが、追加 されたメンバーがあるときはどうしようもありません。この場合はエラーと して通知される必要があるかも知れません。

次に、一つのアプリケーションはファイルなどのやり取りをするとき、その アプリケーションが必要とするクラスのオブジェクトについてのみ興味があり、 任意のクラスのオブジェクトを読む必要はありません。すべての クラスの読み書きが出来ることは要求されません。クラスをカテゴリーに 区分したとき、そのアプリケーションが必要とするカテゴリーの数はそれほど 多くないはずです。KONOEではカテゴリー単位でクラスオブジェクトの生成を 管理します。カテゴリーマネージャを導入します。

2.メンバーデータの識別

メンバーデータは、コーディング上の名前(プログラム内でつけられた 変数名)と、バイトストリーム中での識別子の両方が正しく対応する 必要があります。メンバー名と識別子の対応を行なうメソッドが必要です。

メンバーデータには整数や実数、文字列のようなプリミティブとクラス オブジェクトの場合があります。オブジェクトがメンバーデータとして ローカルに保持されている場合はそのコピーをストリームにおとしたり、 ストリームから読み込んでオブジェクトを生成します。

注意が必要なのはポインターメンバーです。ポインターメンバーはその 使われ方によって処理を考える必要があります。

ポインターをリンクとして保存する場合、相手のオブジェクトを一般的に 参照する仕組みが必要です。これは、例えば、カテゴリー、クラスのほか オブジェクト識別子が必要です。

3.ストリームフォーマット

ファイルなどに書き込まれる書式は、一定の冗長性を持ったものである 必要があります。正しく読み込まれていることの確認や、そうでなかった 場合のデータの復旧が可能でなければなりません。

ここでは次のようなレコードフォーマットを提案します。

  1. 32ビットワードを単位とする。
  2. オブジェクト記録は3ワードからなるヘッダーワードで始まり、 つづいてデータを並べる。
  3. ヘッダーワードの第一語は記録深度(31:24)、カテゴリー識別子(23:16)、 クラス識別子(15:8)、クラスバージョン識別子(7:0)から構成される。
  4. ヘッダーワードの第二語はメンバー識別子(31:24)と データ要素数をあたえる。
  5. ヘッダーワードの第三語はデータ記憶域のサイズを32ビットワードを 1として与える。
  6. データは順に32ビットワードにアラインして記録される。

3.1.ヘッダーワード第一語

+0:
記録深度カテゴリー識別子クラス識別子 クラスバージョン識別子

記録深度は入れ子になった構造体記録の深さを示すもので、最も外側の ものを'A'(0x41)とします。内側に向かって'B'、'C'と増えていきます。 このため、ファイルの場合最初のバイトは'A'でなければなりません。 これにより、KONOE記録ファイルであるかどうか、バイトスワップが必要か どうかを表します。

カテゴリー識別子はクラスカテゴリーを示すデータで、システム全体で ユニークでなければなりません。

カテゴリー識別子は8ビットであらわされ、それぞれクラスカテゴリーを 特定します。特殊なものとして、0はその記録がプリミティブ型のもので あること、1はオブジェクト配列であることを表します。

クラス識別子はその記録がどのクラスのオブジェクトであるかを与えます。 記録の中からクラスの継承関係を読み取るために、また、メンバーが どの継承段階で定義されたものかを明示するために、親クラスのデータは 継承クラスの中に入れ子になって記録されるようにします。そのためには バージョン識別子の一部を使って継承を表します。

クラス識別子はそのクラスカテゴリーの中で固有の値を持ちます。 プリミティブ型の場合は

をそれぞれあらわします。

バージョン識別子はクラスバージョンを識別します。プリミティブでも 意味がある場合がありえます。8ビットが割り当てられていますが、最上位 ビットは継承であり保持ではないことを示すフラッグとして使用します。

3.2.ヘッダーワード第二語

+1:
メンバー識別子記録要素数

メンバーの識別は8ビットで行なわれます。 これは最大256までのメンバーしか記録できないことを示します。

記録要素数は下24ビットで表されます。最大1600万までしか配列を 記述できないことを示します。オブジェクト記録の場合この部分は メンバーの数になります。

3.3.ヘッダーワード第三語

+2:
レコードサイズ

レコードサイズはそのレコード(記録)の長さのうち、ヘッダーワードを 除いた32ビットワード数を表します。この意味は バッファ上で、レコードサイズワードの次をさしているポインターに この長さを加えると次のヘッダーワードをさすことになるようになって いるということです。

3.4.データレコード

+3:
実際のデータ

プリミティブの場合は直接データが書かれます。クラスオブジェクトの場合 最初のメンバーのヘッダーがここに来ます。可変長配列の場合最初の オブジェクトのヘッダーがここに来ます。

オブジェクトポインターの場合、何らかの機構が、オブジェクトを識別する タグを認識し、該当するオブジェクトへのC++ポインターを返してくれ なければなりません。カテゴリー、クラス、オブジェクトIDの情報から 判定します。また、これらの番号はさまざまなところでコンシステントに 管理される必要があります。

3.5.具体例

ここまでで一通りのフォーマットを説明してきました。具体例を見てみましょう。 次のようなクラスを記述するのに、
	class A {
		int i;
		double d[2];
		};
実際にはこのクラスAはカテゴリー2の3番目のクラスとすると、

'A'230深さ1カテゴリー2クラス3バージョン0
01メンバー0要素数2
11 レコードサイズ
'B'010 深さ2プリミティブ型0整数型1バージョン0
01メンバー0要素数1
1レコードサイズ1
1234実際の整数型データ
'B'050 深さ2プリミティブ型0倍精度5バージョン0
12メンバー1要素数2
4レコードサイズ4
XXXX最初の要素の上半分
XXXX最初の要素の下半分
YYYY二番目の要素の上半分
YYYY二番目の要素の下半分

ここで一番外側のメンバー識別は意味がありませんので、別の用途に使えるで しょう。クラスメンバーにクラスオブジェクトがある場合は'B'の次が'L'では なくて'1'となり、そのメンバーは深さ'C'で続いていくことになります。

3.6.トレイラレコードの検討

この形式では先頭から順に追跡することはできますが、後から前に逆に追跡 することが出来ません。考え方としてはレコードトレイラをつけることが 考えられます。例えば'A''0'は'A''1'のトレイラとしてレコードサイズと 直前のレコードのサイズをデータとして持たすことで、逆方向への検索を 行なうことも可能です。(FORTRAN記録を想像してください。)

4.ストリームとストリーマブル

これまでに見てきたフォーマットを実装する方法を考えます。実際の オブジェクトと平板なバイトストリームの関係が課題です。もっとも簡単な 実装として次のようなものが考えられます。オブジェクトをメモリー 上に展開する方法です。まず出力。
  1. オブジェクトのサイズからストリームのサイズを計算する。
  2. バッファを割り付け、ポインターを初期化する。
  3. 各々のメンバーをバッファにコピーし、ポインターを更新する。
  4. 完成したバッファを送り出す。
  5. バッファを開放する。
入力では、
  1. ヘッダーを読み込む。
  2. レコードサイズを読み込む。
  3. 要素数を読み込む。
  4. 必要なサイズのバッファを割り付け、ポインターを初期化する。
  5. データをコピーする。
  6. オブジェクトをnewし、メンバーをコピーする。ポインターメンバーの 場合実体を割り付けたうえでコピーする。
  7. バッファを開放する。
この場合、どのようなクラスやメソッドが必要か考えてみましょう。当然 バッファを管理する必要があります。そのバッファと入出力とを結び付ける ことでストリームを実現します。そのため、ストリームベースクラスを 考えます。

次に、オブジェクトのサイズを計算したり、個別のメンバーデータと バッファの間のコピーを行なうのはそれぞれのオブジェクトでなければ なりません。どのようなメンバーがあるかを知っているのはそのオブジェクト だけだからです。それゆえ、そのオブジェクトはストリーマブルクラスの 継承クラスでなければなりません。

4.1.KonoeObjectStreamBaseクラス

具体的にクラスを考えていきましょう。次のようなプログラムが書けることを 期待しています。
	KonoeObjectStreamBase & os;
	KonoeStreamableObject * op;

	os << * op;
逆の場合はどうでしょうか。どのようなオブジェクトが取り出されるかは 読んでみないとわかりません。ヘッダーブロックを見て、そのクラス識別子 からオブジェクトを作るクラスを理解しなければなりません。

	class KonoeObjectStreamBase {
	public:
		int put( KonoeStreamableObject * op );
			// オブジェクトを書き出す。
		int test( );
			// 次のオブジェクトのヘッダーをテストする。
		int getCategory( );
			// カテゴリー識別子を読む。
		int getClassID( );
			// クラス識別子を読む。
	};

このようにオブジェクトストリームでは入力と出力の動作がかなり変わってきます。 その意味では、KonoeObjectStreamBaseをさらに継承して入力と出力をそれぞれ 用意することに意味があります。

4.2.KonoeInputObjectStreamクラス

入力を行なうためのストリームをKonoeObjectStreamBaseから継承します。

4.3.KonoeOutputObjectStreamクラス

出力を行なうためのストリームをKonoeObjectStreamBaseから継承します。

4.4.KonoeStreamableObjectクラス

こちらは実際に読み書きされるほうのクラスが備えるべき機能を記述 します。

	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 );
			// ストリームからの読み出し
	};

4.5.オブジェクトを生成する方法

ここまでの議論で気がつくことは、オブジェクトの読み出しの時、ヘッダーを みた段階ではまだ、そのオブジェクトは作られていないということです。 カテゴリーやクラスの識別から該当するオブジェクトを特定し、生成する 必要がありますが、C++ではクラスはコンパイル時に確定していなければ ならず、実行時に新しいクラスを定義することは出来ません。そのために、 すべてのストリーマブルなクラスはnewにより生成される関数をどこかに 持たなければなりません。

このための方法として、すべてのクラスについて知っているサービス クラスを用意する手もありますが、あまり効率的とは思えません。(本来の オブジェクト指向システムではこのことが実装されていなければなりませんが。) そこで、これまでの議論でも想像できるように、カテゴリー毎にサービス クラスを用意することにします。


Last Modified : 08-Nov-1997.

KONOEコラボレーション konoe-req@konoe.kek.jp