テクニカル ノート


MFC ライブラリ リファレンス
テクニカル ノート 2: 永続オブジェクトのデータ形式

Noteメモ :

次のテクニカル ノートは、最初にオンライン ドキュメントの一部とされてから更新されていません。結果として、一部のプロシージャおよびトピックが最新でないか、不正になります。最新の情報について、オンライン ドキュメントのキーワードで関係のあるトピックを検索することをお勧めします。

ここでは、C++ の永続オブジェクトをサポートする MFC ルーチンと、オブジェクトのデータをファイルに格納するときの形式について説明します。このテクニカル ノートは、マクロ DECLARE_SERIAL および IMPLEMENT_SERIAL を使うクラスだけが対象です。

問題

MFC では永続データを格納するのに、コンパクト バイナリ形式を使います。つまり、複数のオブジェクトのデータをファイル内の 1 か所に隣接させて格納します。このバイナリ形式によってデータの格納方法を決める構造が規定されますが、オブジェクトが保存する実際のデータを用意するのはオブジェクトのメンバ関数 Serialize です。

MFC では構造化上の問題を解決するのに、クラス CArchive を使います。CArchive オブジェクトで提供される永続用コンテキストは、アーカイブの作成時点から CArchive::Close メンバ関数が呼び出されるまで持続されます。つまり、この関数をプログラマが明示的に呼び出すか、CArchive を含むスコープの終了時にデストラクタが暗黙的に呼び出すまで持続します。

このテクニカル ノートでは、CArchive のメンバ ReadObjectWriteObject の動作について説明します。ReadObjectWriteObject は直接呼び出されず、クラス固有のタイプ セーフな出力ストリーム演算子および入力ストリーム演算子によって使われます。これらの演算子はマクロ DECLARE_SERIAL および IMPLEMENT_SERIAL によって自動的に生成されます。

class CMyObject : public CObject
{
    DECLARE_SERIAL(CMyObject)
};

IMPLEMENT_SERIAL(CMyObj, CObject, 1)

// example usage (ar is a CArchive&)
CMyObject* pObj;
CArchive& ar;
ar << pObj;        // calls ar.WriteObject(pObj)
ar >> pObj;        // calls ar.ReadObject(RUNTIME_CLASS(CObj))

このテクニカル ノートでは MFC のソース ファイル ARCOBJ.CPP に含まれているコードについて説明します。CArchive の主要部分の実装については、ARCCORE.CPP を参照してください。

オブジェクトを記憶領域に格納する (CArchive::WriteObject)

CArchive::WriteObject メンバ関数はオブジェクトの再構築時に使うヘッダー データを書き込みます。このデータの内容は 2 つの部分、つまりオブジェクトの型とオブジェクトの状態に分けられます。WriteObject メンバ関数は書き込むオブジェクトの識別記号も保持するので、オブジェクトを指す循環ポインタなどのポインタの数に関係なく、単一のコピーが保存されます。

オブジェクトの保存 (出力) および復元 (入力) には複数の "記号定数" を使います。記号定数は値であり、バイナリ形式で格納され、以下の情報をアーカイブに提供します。プリフィックス "w" は 16 ビット値であることを示します。

タグ 説明

wNullTag

NULL オブジェクト ポインタとして使用します (0)。

wNewClassTag

次のクラス記述がこのアーカイブ コンテキストで初めてであることを示します (-1)。

wOldClassTag

読み込むオブジェクトのクラスがこのコンテキストに既にあったことを示します (0x8000)。

オブジェクトを格納するとき、アーカイブには CMapPtrToPtr (m_pStoreMap) が保持されます。これによって、格納するオブジェクトと 32 ビットの永続識別子 (PID) 間がリンクされます。PID は一意なオブジェクトにそれぞれ付与されます。アーカイブのコンテキストに保存される一意なクラス名にもそれぞれ付与されます。これらの PID は 1 から順に付与されます。これらの PID の有効範囲はアーカイブのスコープ内に限られます。特に、レコード番号などの識別項目と混同しないように注意してください。

MFC Version 4.0 以降では、CArchive クラスが拡張されているので、大型のアーカイブをサポートできます。以前のバージョンでは PID が 16 ビット値だったので、アーカイブできるオブジェクト数が 0x7FFE (32766) でした。現在の PID は 32 ビットですが、0x7FFE 以下の場合は 16 ビット値で書き出されます。大型の PID は 0x7FFF として書き出され、その後に 32 ビット PID が続きます。この手法はファイルの下位互換性を保つためのものです。

オブジェクトの保存先としてアーカイブを指定すると (通常、グローバル出力ストリーム演算子によって)、CObject ポインタが NULL かどうかがチェックされます。

ポインタが NULL であると、wNullTag がアーカイブ ストリームに挿入されます。シリアル化可能なリアル オブジェクト ポインタ (DECLARE_SERIAL クラス) があれば、m_pStoreMap をチェックして、このオブジェクトが保存済みかどうかをチェックします。保存済みの場合は、このオブジェクトに関連付けられている 32 ビット PID を挿入します。

未保存のオブジェクトの場合は、2 とおりの可能性を考慮する必要があります。つまり、オブジェクトとその正しい型 (クラス) がこのアーカイブ コンテキストに初めて出現したものか、またはこのオブジェクトと同じ型が以前出現しているかどうかです。既に出現した型かどうかを判定するには、m_pStoreMap に問い合わせて、保存するオブジェクトに対応付けられている CRuntimeClass オブジェクトに一致する CRuntimeClass オブジェクトの有無を調べます。このクラスが出現済みの場合は、WriteObject が wOldClassTag とこのインデックスのビットごとの OR をとったタグを挿入します。CRuntimeClass がこのアーカイブ コンテキストに初めて出現したときは、WriteObject がこのクラスに新しい PID を付与し、アーカイブの wNewClassTag 値の次に挿入されます。

次に、CRuntimeClass のメンバ関数 Store を使って、このクラスの記述子をアーカイブに挿入します。CRuntimeClass::Store はクラスのスキーマ番号と ASCII テキスト名を挿入します。ASCII テキスト名を使うと、アプリケーション間でアーカイブの一意性が保証されないので、データ ファイルの破損防止のためにファイルにタグを付けることをお勧めします。クラス情報が挿入されると、アーカイブではオブジェクトを m_pStoreMap に設定してから、Serialize メンバ関数を呼び出してクラス固有のデータをアーカイブに挿入します。オブジェクトを m_pStoreMap に設定してから Serialize を呼び出すと、記憶領域にオブジェクトのコピーが複数保存されません。

最初の呼び出し側 (通常はオブジェクト ネットワークのルート) に戻るときは、忘れずにアーカイブを Close してください。ほかの CFile 操作を行うときは、必ず CArchive のメンバ関数 Flush を呼び出してください。この関数を呼び出さないと、アーカイブが破損します。

Noteメモ :

この実装では、ハードウェア上アーカイブ コンテキストあたりの最大インデックス数が 0x3FFFFFFE に制限されています。この上限値は、単一アーカイブに保存できる一意なオブジェクトおよびクラスの最大数です。ただし、単一ディスク ファイルに保存できるアーカイブ コンテキストの数は無制限です。

記憶領域からオブジェクトを読み込む (CArchive::ReadObject)

オブジェクトの読み込み (入力) には、CArchive::ReadObject メンバ関数を使います。この関数は WriteObject の逆の操作を行います。WriteObject と同じように、ReadObject もユーザー コードからは直接呼び出しません。ユーザー コードではタイプ セーフな入力ストリーム演算子を呼び出して、そこから目的の CRuntimeClassReadObject を呼び出します。これにより、入力ストリーム演算における型の整合性が保証されます。

WriteObject は 1 (0 は NULL オブジェクト用に定義済み) からの昇順 PID を付与するので、ReadObject では配列を使ってアーカイブ コンテキストの状態を保守できます。PID を記憶領域から読み出した PID が m_pLoadArray の現在の上限より大きいと、ReadObject は次のオブジェクトが新規オブジェクト (またはクラス記述) であると認識します。

スキーマ番号

スキーマ番号は、クラスの IMPLEMENT_SERIAL に遭遇すると、そのクラスに付与されます。この番号は、クラスの実装 "バージョン" を示します。スキーマはクラスの実装を表すものであり、特定のオブジェクトのこれまでの永続回数を示すものではありません。オブジェクトの永続回数は通常、オブジェクト バージョンと言います。

同一クラスの複数の実装を長期間保持するには、オブジェクトの Serialize メンバ関数の実装を改訂するたびに、スキーマ番号をインクリメントします。以前のバージョンの実装で格納したオブジェクトを読み込みできます。

CArchive::ReadObject メンバ関数は、永続記憶領域にあるスキーマ番号がメモリ内のクラス内容のスキーマ番号と違うことを検出すると、CArchiveException を発行します。これは回復困難な例外です。

VERSIONABLE_SCHEMA とスキーマ バージョンの OR をとると、この例外のスローを防止できます。VERSIONABLE_SCHEMA を使うと、コードでは CArchive::GetObjectSchema からの戻り値を調べて Serialize 関数で該当する対策をとることができます。

Serialize 関数を直接呼び出す

WriteObjectReadObject の全般オブジェクト アーカイブ方式のオーバーヘッドが不要または望ましくないケースは少なくありません。これは、データを CDocument にシリアル化する場合に共通する問題です。このような場合は入力ストリーム演算子も出力ストリームも使わず、CDocumentSerialize メンバ関数を直接呼び出します。文書の内容はさらに汎用的なオブジェクト アーカイブ方式を使うこともできます。

Serialize を直接呼び出すと、以下の利点と不具合があります。

  • オブジェクトをシリアル化する前または後に、アーカイブに補足バイトが追加されません。したがって、保存データを小型化できるばかりでなく、どのファイル形式でも処理できる Serialize ルーチンを実装できます。

  • ほかの目的のためにより汎用的なオブジェクト アーカイブ方式が必要な場合以外は、WriteObject および ReadObject と関連コレクションがアプリケーションにリンクされないように MFC が微調整されます。

  • コードでは旧スキーマ番号から復元する必要がありません。したがって、スキーマ番号、ファイル形式バージョン番号など、データ ファイルの先頭に必要なあらゆるマジック番号のエンコーディングは文書のシリアル化コードが行います。

  • Serialize を直接呼び出してシリアル化するオブジェクトでは、CArchive::GetObjectSchema を使えません。使った場合は、未知バージョンを示す戻り値 (UINT)-1 を処理する必要があります。

Serialize は直接文書に対して呼び出されるので、通常文書のサブオブジェクトではその親文書への参照をアーカイブできません。サブオブジェクトにはそれぞれのコンテナ オブジェクトへのポインタを明示的に渡すか、バック ポインタのアーカイブ前に CArchive::MapObject 関数で CDocument ポインタを PID に割り当てる必要があります。

前に述べたように、Serialize を直接呼び出すときは、バージョン情報とクラス情報を自分でエンコーディングしてください。形式を後で変更しても、旧ファイルとの下位互換性は維持されます。CArchive::SerializeClass 関数を明示的に呼び出してから、オブジェクトを直接シリアル化することも、基本クラスを呼び出すこともできます。

参照

その他の技術情報

番号順テクニカル ノート
カテゴリ別テクニカル ノート

タグ :


Page view tracker