次の方法で共有


TN002: 永続性オブジェクトデータ形式

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

問題

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

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

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

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 オブジェクトのm_pStoreMapを照会します。 一致するものがある場合、WriteObjectOR とこのインデックスのビットごとのであるタグを挿入します。 CRuntimeClassがこのアーカイブ コンテキストの新しい場合、WriteObjectはそのクラスに新しい PID を割り当て、それをアーカイブに挿入し、その前に wNewClassTag 値を付けます。

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

最初の呼び出し元 (通常はオブジェクトのネットワークのルート) に戻る場合は、 CArchive::Close を呼び出す必要があります。 他の CFile 操作を実行する場合は、アーカイブの破損を防ぐために、 CArchive メソッド Flush を呼び出す必要があります。

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

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

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

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

スキーマ番号

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

同じクラスの複数の異なる実装を一定期間にわたって維持する場合は、オブジェクトの Serialize メソッドの実装を変更するときにスキーマをインクリメントすることで、古いバージョンの実装を使用して格納されたオブジェクトを読み込むことができるコードを記述できます。

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

VERSIONABLE_SCHEMAをスキーマバージョンと組み合わせて (ビット単位のORを使用) することで、この例外が発生しないようにすることができます。 VERSIONABLE_SCHEMAを使用すると、Serialize からの戻り値を確認することで、コードの関数で適切なアクションを実行できます。

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

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

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

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

  • MFC は、他の目的のためにより一般的なオブジェクト アーカイブ スキームが必要でない限り、 WriteObject および ReadObject の実装および関連するコレクションがアプリケーションにリンクされないように調整されます。

  • コードは、古いスキーマ番号から回復する必要はありません。 これにより、ドキュメントのシリアル化コードは、スキーマ番号、ファイル形式のバージョン番号、またはデータ ファイルの先頭で使用する識別番号をエンコードする役割を担います。

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

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

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

こちらも参照ください

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