テクニカル ノート 2: 永続オブジェクトのデータ形式

このメモでは、永続 C++ オブジェクトをサポートする MFC ルーチンと、ファイルに格納されるときのオブジェクト データの形式について説明します。 これは、DECLARE_SERIAL マクロおよび IMPLEMENT_SERIAL マクロを持つクラスにのみ適用されます。

問題

永続データの MFC 実装では、ファイルの 1 つの隣接部分に多数のオブジェクトのデータが格納されます。 オブジェクトの Serialize メソッドは、オブジェクトのデータをコンパクトなバイナリ形式に変換します。

この実装により、CArchive クラスを使用して、すべてのデータが確実に同じ形式で保存されます。 この実装では CArchive オブジェクトが変換ツールとして使用されます。 このオブジェクトは、作成時から CArchive::Close 呼び出し時まで永続します。 このメソッドは、プログラマによって明示的に呼び出されるか、またはプログラムが CArchive を含むスコープを終了するときにデストラクターによって暗黙的に呼び出されるかのいずれかです。

このメモでは、CArchive メンバー CArchive::ReadObject および CArchive::WriteObject の実装を説明します。 これらの関数のコードは Arcobj .cpp および Arccore.cpp の主な CArchive 実装にもあります。 ユーザー コードは ReadObject および WriteObject を直接呼び出しません。 代わりに、これらのオブジェクトは、DECLARE_SERIAL および IMPLEMENT_SERIAL マクロによって自動的に生成されるクラス固有のタイプ セーフな挿入演算子と抽出演算子によって使用されます。 次のコードは、WriteObject および ReadObject が暗黙的に呼び出される方法を示しています。

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))

ストアにオブジェクトを保存 (CArchive:: WriteObject)

メソッド CArchive::WriteObject は、オブジェクトの再構築に使用されるヘッダー データを書き込みます。 このデータは、オブジェクトの型とオブジェクトの状態の 2 つの部分で構成されます。 また、このメソッドは、書き込まれるオブジェクトの ID を保持し、そのオブジェクトへのポインターの数 (循環ポインターを含む) に関係なく、1 つのコピーのみが保存されるようにします。

オブジェクトの保存 (挿入) と復元 (抽出) は、複数の "マニフェスト定数" に依存します。これらは、バイナリに格納され、アーカイブに重要な情報を提供する値です ("w" プレフィックスは 16 ビットの数量を示します)。

タグ 説明
wNullTag NULL オブジェクト ポインター (0) に使用されます。
wNewClassTag 次に示すクラスの説明が、このアーカイブ コンテキスト (-1) の新しいクラスであることを示します。
wOldClassTag 読み取り対象のオブジェクトのクラスが、このコンテキスト (0x8000) で認識されたことを示します。

オブジェクトを格納する場合、アーカイブは CMapPtrToPtr (m_pStoreMap) を保持します。これは、格納されているオブジェクトから 32 ビットの永続識別子 (PID) へのマッピングです。 PID は、すべての一意のオブジェクトと、アーカイブのコンテキストで保存されるすべての一意のクラス名に割り当てられます。 これらの PID は、1 から順番に渡されます。 これらの PID はアーカイブの範囲外では意味を持ちません。特に、レコード番号やその他の ID 項目と混同することはありません。

CArchive クラスでは、PID は 32 ビットですが、0x7FFE より大きい場合を除き、16 ビットとして書き込まれます。 大きな PID は、0x7FFF として書き込まれた後に 32 ビットの PID が続きます。 これにより、以前のバージョンで作成されたプロジェクトとの互換性が維持されます。

オブジェクトをアーカイブに保存する要求 (通常はグローバル挿入演算子を使用) が行われると、NULL の CObject ポインターに対するチェックが行われます。 ポインターが NULL の場合、wNullTag がアーカイブ ストリームに挿入されます。

ポインターが NULL ではなく、シリアル化できる (クラスが DECLARE_SERIAL クラスである) 場合、コードは m_pStoreMap をチェックして、オブジェクトが既に保存されているかどうかを確認します。 保存されている場合、コードはそのオブジェクトに関連付けられた 32 ビット PID をアーカイブ ストリームに挿入します。

オブジェクトが以前に保存されていない場合は、次の 2 つの考えられる可能性があります。オブジェクトのオブジェクトと正確な型 (つまり、クラス) がこのアーカイブ コンテキストの新しいものであるか、またはオブジェクトが既に表示されている正確な型であること。 型が検出されたかどうかを判断するために、コードは、保存されているオブジェクトに関連付けられている CRuntimeClass オブジェクトと一致する CRuntimeClass オブジェクトの m_pStoreMap をクエリします。 一致するものがある場合は、WriteObject は、wOldClassTag のビットごとの OR であるタグおよびこのインデックスを挿入します。 CRuntimeClass がこのアーカイブ コンテキストにとって新しいクラスである場合、WriteObject は新しい PID をそのクラスに割り当て、wNewClassTag の前のアーカイブに挿入します。

次に、このクラスの記述子は、CRuntimeClass::Store メソッドを使用してアーカイブに挿入されます。 CRuntimeClass::Store によって、クラスのスキーマ番号 (下記参照) およびクラスの ASCII テキスト名が挿入されます。 ASCII テキスト名を使用しても、アプリケーション間でのアーカイブの一意性は保証されないことに注意してください。 そのため、破損を防ぐためにデータ ファイルにタグを付ける必要があります。 クラス情報を挿入した後、アーカイブはオブジェクトを m_pStoreMap に格納し、Serialize メソッドを呼び出してクラス固有のデータを挿入します。 Serialize を呼び出す前にオブジェクトを m_pStoreMap に配置すると、オブジェクトの複数のコピーがストアに保存されなくなります。

最初の呼び出し元 (通常はオブジェクトのネットワークのルート) に戻るときに、CArchive::Close を呼び出す必要があります。 他の CFile 操作を実行する場合は、アーカイブが破損しないように、CArchive メソッドのフラッシュを呼び出す必要があります。

Note

この実装は、アーカイブ コンテキストごとに0x3FFFFFFE インデックスのハード制限を設けています。 この数は、1 つのアーカイブに保存できる一意のオブジェクトとクラスの最大数を表しますが、1 つのディスク ファイルに格納できるアーカイブ コンテキストの数に制限はありません。

ストアからのオブジェクトの読み込み (CArchive::ReadObject)

オブジェクトの読み込み (抽出) は CArchive::ReadObject メソッドを使用し、これは WriteObject の逆です。 WriteObject と同様に、ReadObject はユーザー コードによって直接呼び出されるわけではありません。ユーザー コードは、予期される CRuntimeClass を使用して ReadObject を呼び出すタイプセーフな抽出演算子を呼び出す必要があります。 これにより、抽出操作の型の整合性が保証されます。

WriteObject の実装は、PID を 1 から始めて増やしながら割り当てるため (0 は NULL オブジェクトとして事前定義されている)、ReadObject の実装は、配列を使用してアーカイブ コンテキストの状態を維持できます。 PID がストアから読み取られるときに、PID が m_pLoadArray の現在の上限を超えている場合、新しいオブジェクト (またはクラスの説明) が続いていることを ReadObject は認識します。

スキーマ番号

クラスの IMPLEMENT_SERIAL メソッドが検出されたときにクラスに割り当てられるスキーマ番号は、クラス実装の "バージョン" です。 スキーマは、特定のオブジェクトが永続化された回数 (通常はオブジェクト バージョンと呼ばれます) ではなく、クラスの実装を表します。

同じクラスの複数の実装を時間の経過と共に維持する場合、オブジェクトの Serialize メソッド実装を変更するときにスキーマを増やすと、以前のバージョンの実装を使用して格納されたオブジェクトを読み込むことができるコードを記述できるようになります。

CArchive::ReadObject メソッドは、メモリ内のクラスの説明のスキーマ番号とは異なるスキーマ番号を永続ストアで検出すると、CArchiveException をスローします。 この例外から回復するのは簡単ではありません。

VERSIONABLE_SCHEMA をスキーマ バージョンと組み合わせて (ビットごとの OR) を使用して、この例外がスローされないようにすることができます。 VERSIONABLE_SCHEMA を使用することにより、コードは、CArchive::GetObjectSchema からの戻り値をチェックすることによって、Serialize 関数内で適切なアクションを実行できます。

シリアル化を直接呼び出す

多くの場合、WriteObjectReadObject の一般的なオブジェクト アーカイブ スキームのオーバーヘッドは必要ありません。 これは、データを CDocument にシリアル化する場合の一般的なケースです。 この場合、CDocumentSerialize メソッドは、extract 演算子または insert 演算子を使用せずに、直接呼び出されます。 ドキュメントの内容によって、より一般的なオブジェクト アーカイブ スキームが使用される場合があります。

直接 Serialize を呼び出すと、次のような長所と短所があります。

  • オブジェクトをシリアル化する前または後に、追加のバイトがアーカイブに追加されることはありません。 これにより、保存されたデータが小さくなるだけでなく、任意のファイル形式を処理できる Serialize ルーチンを実装できるようになります。

  • MFC はチューニングされているため、WriteObject および ReadObject の実装と関連するコレクションは、他の目的でより一般的なオブジェクト アーカイブ スキームが必要な場合を除き、アプリケーションにリンクされません。

  • コードは、古いスキーマ番号から復旧する必要はありません。 これにより、スキーマ番号、ファイル形式のバージョン番号、またはデータ ファイルの開始時に使用する番号を識別するドキュメントをエンコードするシリアル化コードが作成されます。

  • Serialize を直接呼び出してシリアル化されたオブジェクトは CArchive::GetObjectSchema を使用してはいけません。また、バージョンが不明であることを示す (UINT)-1 の戻り値を処理する必要があります。

Serialize はドキュメントで直接呼び出されるため、通常、ドキュメントのサブオブジェクトがその親ドキュメントへの参照をアーカイブすることはできません。 これらのオブジェクトには、コンテナー ドキュメントへの明示的なポインターを与えるか、または、これらのバック ポインターをアーカイブする前に、CArchive:: MapObject 関数を使用して CDocument ポインターを PID にマップする必要があります。

既に説明したように、Serialize を直接呼び出すときは、バージョン情報とクラス情報を自分でエンコードする必要があります。これにより、古いファイルとの下位互換性を維持したまま、後で形式を変更できます。 CArchive::SerializeClass 関数は、オブジェクトを直接シリアル化する前に、または基底クラスを呼び出す前に明示的に呼び出すことができます。

関連項目

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