Theolizer  Version.1.2.0
serializer for C++ / Do you want to update your classes easily ?
オブジェクト追跡について

ここでは、オブジェクト追跡について説明します。


1.オブジェクト追跡の使い方


1-1.指定方法

・ポインタ型の指定(通常通りのシリアライズ指定です)

  1. メンバ変数の場合は、通常通り保存指定
  2. THEOLIZER_PROCESS()マクロ

・オーナー・ポインタ型の指定(オーナー指定)

  1. メンバ変数の場合は、THEOLIZER_ANNOTATE(FS:...<...>Owner)
    このアノテーションはポインタに対してのみ指定できます。
  2. THEOLIZER_PROCESS_OWNER()マクロ
    このマクロはボインタのみ指定できます。

また、回復時は初期値が必要です。nullptr、もしくは、適正なインスタンスへのポインタを設定して下さい。ポイント先インスタンスのクラスとシリアライズ・データ内のクラスが同じならば設定されていたインスタンスを維持したまま回復します。(これは保存先指定機能により複数のファイルに別々に保存されたメンバを統合するための機能です。) もし、クラスが異なる場合は解放し、シリアライズ・データと同じクラスをnewして回復します。nullptrの時も同様です。

・オブジェクト追跡指定

  1. メンバ変数の場合は、THEOLIZER_ANNOTATE(FS:...<...>Pointee)
  2. THEOLIZER_PROCESS_POINTEE()マクロ

1-2.サンプル・ソース

サンプル用のクラス定義:(source/reference_and_test/basic2/test_object_tracking.h)
(静的定義領域、動的生成領域については3-3.オブジェクト追跡で用いる用語 を参照下さい。)

class ObjectTrackingClass
{
// 静的定義領域
int mInt THEOLIZER_ANNOTATE(FS:<>Pointee);
// 動的生成領域
short* mShort THEOLIZER_ANNOTATE(FS:<>Owner);
public:
ObjectTrackingClass() :
mInt{0},
mShort{nullptr}
{ }
ObjectTrackingClass(int iInt, short iShort) :
mInt{iInt},
mShort{new short{iShort}}
{ }
~ObjectTrackingClass()
{
delete mShort;
}
void check(int iInt, short iShort)
{
THEOLIZER_EQUAL(mInt, iInt);
THEOLIZER_EQUAL(*mShort, iShort);
}
int* getStatic() { return &mInt; }
short* getDynamic() { return mShort; }
THEOLIZER_INTRUSIVE(CS, (ObjectTrackingClass), 1);
};

ObjectTrackingClass全体はオブジェクト追跡するクラスのサンプルです。また、メンバ変数はTHEOLIZER_ANNOTATE()マクロによるオブジェクト追跡関連指定のサンプルです。

  • mIntメンバ変数はオブジェクト追跡するよう指定されたメンバ変数です。
  • mShortメンバ変数はオーナー指定されたメンバ変数です。

保存処理:(source/reference_and_test/basic2/test_object_tracking.cpp)

{
// ---<<< シリアライズ対象変数定義 >>>---
// 静的定義領域(プリミティブ)
long aLong{100};
// 静的定義領域(クラス)、動的生成領域(メンバ変数)
ObjectTrackingClass aObjectTrackingClass{200, 300};
// 静的定義領域を指すポインタ(非オーナー)
long* aLongPtr=&aLong;
ObjectTrackingClass* aObjectTrackingClassPtr=&aObjectTrackingClass;
// 動的生成領域を指すオーナー・ポインタ
long* aLongOwner=new long{101};
ObjectTrackingClass* aObjectTrackingClassOwner=new ObjectTrackingClass{201, 301};
auto temp0=makeAutoRelease(aLongOwner); // 自動解放設定
auto temp1=makeAutoRelease(aObjectTrackingClassOwner); // 自動解放設定
// 動的生成領域を指すポインタ(非オーナー)
long* aLongPtr2=aLongOwner;
ObjectTrackingClass* aObjectTrackingClassPtr2=aObjectTrackingClassOwner;
// THEOLIZER_ANNOTATE()でオブジェクト追跡指定された静的定義領域を指すポインタ(非オーナー)
int* aIntPtr=aObjectTrackingClassOwner->getStatic();
// THEOLIZER_ANNOTATE()でオーナー指定された動的生成領域を指すポインタ(非オーナー)
short* aShortPtr=aObjectTrackingClassOwner->getDynamic();
// ---<<< 保存処理 >>>---
std::ofstream aStream("tutorise_object_tracking.json");
theolizer::JsonOSerializer<> aSerializer(aStream);
// 静的定義領域を指すポインタ(非オーナー)を保存
THEOLIZER_PROCESS(aSerializer, aLongPtr);
THEOLIZER_PROCESS(aSerializer, aObjectTrackingClassPtr);
// 静的定義領域(プリミティブ)を保存
THEOLIZER_PROCESS_POINTEE(aSerializer, aLong);
// 静的定義領域(クラス)、動的生成領域(メンバ変数mShort)を保存
THEOLIZER_PROCESS_POINTEE(aSerializer, aObjectTrackingClass);
// 動的生成領域を指すオーナー・ポインタを保存
THEOLIZER_PROCESS_OWNER(aSerializer, aLongOwner);
THEOLIZER_PROCESS_OWNER(aSerializer, aObjectTrackingClassOwner);
// 動的生成領域を指すポインタ(非オーナー)を保存
THEOLIZER_PROCESS(aSerializer, aLongPtr2);
THEOLIZER_PROCESS(aSerializer, aObjectTrackingClassPtr2);
// THEOLIZER_ANNOTATE()で指定したインスタンスを指すボインタ(非オーナー/オーナー)を保存
THEOLIZER_PROCESS(aSerializer, aIntPtr);
THEOLIZER_PROCESS(aSerializer, aShortPtr);
// オブジェクトIDテーブルのクリア
aSerializer.clearTracking();
}

makeAutoRelease()
これはnewで獲得した領域を自動的にdeleteするためのヘルパー関数です。戻り値のインスタンスが解放されるタイミングでdeleteします。source/reference_and_test/basic/common.hで定義しています。たいへん小さいですので、興味のある方は覗いてみて下さい。

これにより、下図のようなデータ構造が生成され、図の順序でデータが保存されます。

object_tracking1.png

tutorise_object_tracking.jsonファイルは下記となります。 (//以下は説明のために書き込みました。)

{
    "SerialzierName":"JsonTheolizer",
    "GlobalVersionNo":1,
    "TypeInfoList":[1]
}
1                           // オブジェクトID=1へのポインタ(aLongPtr)
2                           // オブジェクトID=2へのポインタ(aObjectTrackingClassPtr)
[1,100]                     // オブジェクトID=1のインスタンス(aLong)
[2,{                        // オブジェクトID=2のインスタンス(aObjectTrackingClass)
    "mInt":[3,200],         // オブジェクトID=3のインスタンス(mInt)
    "mShort":[4,300]        // オブジェクトID=4のインスタンス(mShort)
}]
[5,101]                     // オブジェクトID=5のインスタンス(aLongOwner)
[6,"ObjectTrackingClass",{  // オブジェクトID=6のインスタンス(aObjectTrackingOwner)
    "mInt":[7,201],         // オブジェクトID=7のインスタンス(mInt)
    "mShort":[8,301]        // オブジェクトID=8のインスタンス(mShort)
}]
5                           // オブジェクトID=5へのポインタ(aLongPtr2)
6                           // オブジェクトID=6へのポインタ(aObjectTrackingClassPtr2)
7                           // オブジェクトID=7へのポインタ(aIntPtr)
8                           // オブジェクトID=8へのポインタ(aShortPtr)

回復処理:(source/reference_and_test/basic2/test_object_tracking.cpp)
元のデータ構造を回復できていることをチェックしています。

{
// ---<<< 回復先変数定義 >>>---
// 静的定義領域(プリミティブ)
long aLong{0};
// 静的定義領域(クラス)、動的生成領域(メンバ変数)
ObjectTrackingClass aObjectTrackingClass;
// 静的定義領域を指すポインタ(非オーナー)
long* aLongPtr=nullptr;
ObjectTrackingClass* aObjectTrackingClassPtr=nullptr;
// 動的生成領域を指すオーナー・ポインタ
long* aLongOwner=nullptr;
ObjectTrackingClass* aObjectTrackingClassOwner=nullptr;
auto temp0=makeAutoRelease(aLongOwner); // 自動解放設定
auto temp1=makeAutoRelease(aObjectTrackingClassOwner); // 自動解放設定
// 動的生成領域を指すポインタ(非オーナー)
long* aLongPtr2=nullptr;
ObjectTrackingClass* aObjectTrackingClassPtr2=nullptr;
// THEOLIZER_ANNOTATE()でオブジェクト追跡指定された静的定義領域を指すポインタ(非オーナー)
int* aIntPtr=nullptr;
// THEOLIZER_ANNOTATE()でオーナー指定された動的生成領域を指すポインタ(非オーナー)
short* aShortPtr=nullptr;
// ---<<< 回復処理 >>>---
std::ifstream aStream("tutorise_object_tracking.json");
theolizer::JsonISerializer<> aSerializer(aStream);
// 静的定義領域を指すポインタ(非オーナー)を回復
THEOLIZER_PROCESS(aSerializer, aLongPtr);
THEOLIZER_PROCESS(aSerializer, aObjectTrackingClassPtr);
// 静的定義領域(プリミティブ)を回復
THEOLIZER_PROCESS_POINTEE(aSerializer, aLong);
// 静的定義領域(クラス)、動的生成領域(メンバ変数mShort)を回復
THEOLIZER_PROCESS_POINTEE(aSerializer, aObjectTrackingClass);
// 動的生成領域を指すオーナー・ポインタを回復
THEOLIZER_PROCESS_OWNER(aSerializer, aLongOwner);
THEOLIZER_PROCESS_OWNER(aSerializer, aObjectTrackingClassOwner);
// 動的生成領域を指すポインタ(非オーナー)を回復
THEOLIZER_PROCESS(aSerializer, aLongPtr2);
THEOLIZER_PROCESS(aSerializer, aObjectTrackingClassPtr2);
// THEOLIZER_ANNOTATE()で指定したインスタンスを指すボインタ(非オーナー/オーナー)を回復
THEOLIZER_PROCESS(aSerializer, aIntPtr);
THEOLIZER_PROCESS(aSerializer, aShortPtr);
// オブジェクトIDテーブルのクリア
aSerializer.clearTracking();
// ---<<< 結果のチェック >>>---
// 静的定義領域を指すポインタ(非オーナー)
THEOLIZER_EQUAL_PTR(aLongPtr, &aLong);
THEOLIZER_EQUAL_PTR(aObjectTrackingClassPtr, &aObjectTrackingClass);
// 静的定義領域(プリミティブ)
THEOLIZER_EQUAL(aLong, 100);
// 静的定義領域(クラス)、動的生成領域(メンバ変数)
aObjectTrackingClass.check(200, 300);
// 動的生成領域を指すオーナー・ポインタ
THEOLIZER_EQUAL(*aLongOwner, 101);
aObjectTrackingClassOwner->check(201, 301);
// 動的生成領域を指すポインタ(非オーナー)
THEOLIZER_EQUAL_PTR(aLongPtr2, aLongOwner);
THEOLIZER_EQUAL_PTR(aObjectTrackingClassPtr2, aObjectTrackingClassOwner);
// THEOLIZER_ANNOTATE()で指定したインスタンスを指すボインタ(非オーナー/オーナー)
THEOLIZER_EQUAL_PTR(aIntPtr, aObjectTrackingClassOwner->getStatic());
THEOLIZER_EQUAL_PTR(aShortPtr, aObjectTrackingClassOwner->getDynamic());
}


2.ポリモーフィズムの使い方

3-1.使い方

ポリモーフィックな基底クラスへのポインタをオーナ・ポインタとしてシリアライズすることで、ポリモーフィックに回復されます。つまり、派生クラスのインスタンスを基底クラスへのポインタでポイントして保存して回復した場合、派生クラスのインスタンスとして回復されます。

そのように動作させるために、2つの注意点があります。

  1. 基底クラスは仮想関数を最低1つ持つ必要が有ります。
    これがないと、C++の仕様上、基底クラスへのポインタが指す派生クラスのインスタンスの型を動的に判定できないためです。

  2. 派生クラスはTHEOLIZER_REGISTER_CLASS()マクロで派生クラスであることを指定して下さい。
    基底クラスへのポインタ経由でシリアライズしたい派生クラスは必ず指定して下さい。
    指定漏れすると、シリアライズ処理する時に"Can not find the derived class for <基底クラス>."エラーになります。

また、1-6-4.ポリモーフィズムにおける制約事項 も参照下さい。

サンプル用のクラス定義:(source/reference_and_test/basic2/test_polymorphism.h)

//----------------------------------------------------------------------------
// 基底クラス
//----------------------------------------------------------------------------
struct PolyBase
{
int mInt;
PolyBase(int iInt) : mInt(iInt) { }
virtual ~PolyBase() { }
virtual void check()=0;
};
//----------------------------------------------------------------------------
// 派生クラス
//----------------------------------------------------------------------------
struct PolyDerived0 : public PolyBase
{
short mShort;
PolyDerived0() : PolyBase(0), mShort(0) { }
PolyDerived0(bool) : PolyBase(100), mShort(200) { }
void check()
{
THEOLIZER_EQUAL(mInt, 100);
THEOLIZER_EQUAL(mShort, 200);
}
};
THEOLIZER_REGISTER_CLASS((PolyDerived0));
struct PolyDerived1 : public PolyBase
{
std::string mString;
PolyDerived1() : PolyBase(0), mString("") { }
PolyDerived1(bool) : PolyBase(1000), mString("string") { }
void check()
{
THEOLIZER_EQUAL(mInt, 1000);
THEOLIZER_EQUAL(mString, "string");
}
};
THEOLIZER_REGISTER_CLASS((PolyDerived1));

THEOLIZER_REGISTER_CLASS()による派生クラスの指定を忘れないようにお願いします。
また、クラス名を()で囲って指定する必要が有りますのでご注意下さい。

保存処理:(source/reference_and_test/basic2/test_polymorphism.cpp)

{
// ---<<< シリアライズ対象変数定義 >>>---
PolyBase* aPolyBase0=new PolyDerived0{true};
PolyBase* aPolyBase1=new PolyDerived1{true};
auto temp0=makeAutoRelease(aPolyBase0); // 自動解放設定
auto temp1=makeAutoRelease(aPolyBase1); // 自動解放設定
// ---<<< 保存処理 >>>---
std::ofstream aStream("tutorise_polymorphism.json");
theolizer::JsonOSerializer<> aSerializer(aStream);
// オーナーとして保存/回復する
THEOLIZER_PROCESS_OWNER(aSerializer, aPolyBase0);
THEOLIZER_PROCESS_OWNER(aSerializer, aPolyBase1);
// オブジェクトIDテーブルのクリア
aSerializer.clearTracking();
}

回復処理:(source/reference_and_test/basic2/test_polymorphism.cpp)
元のデータ構造を回復できていることをチェックしています。

{
// ---<<< 回復先変数定義 >>>---
PolyBase* aPolyBase0=nullptr;
PolyBase* aPolyBase1=nullptr;
auto temp0=makeAutoRelease(aPolyBase0); // 自動解放設定
auto temp1=makeAutoRelease(aPolyBase1); // 自動解放設定
// ---<<< 回復処理 >>>---
std::ifstream aStream("tutorise_polymorphism.json");
theolizer::JsonISerializer<> aSerializer(aStream);
// オーナーとして保存/回復する
THEOLIZER_PROCESS_OWNER(aSerializer, aPolyBase0);
THEOLIZER_PROCESS_OWNER(aSerializer, aPolyBase1);
// オブジェクトIDテーブルのクリア
aSerializer.clearTracking();
// ---<<< 結果のチェック >>>---
aPolyBase0->check();
aPolyBase1->check();
}

tutorise_object_tracking.jsonファイルは下記となります。

{
    "SerialzierName":"JsonTheolizer",
    "GlobalVersionNo":1,
    "TypeInfoList":[1]
}
[1,"PolyDerived0",{
    "(PolyBase)":{
        "mInt":100
    },
    "mShort":200
}]
[2,"PolyDerived1",{
    "(PolyBase)":{
        "mInt":1000
    },
    "mString":"string"
}]

メンバ変数名は"メンバ変数名"として記録されます。基底クラスにはメンバ変数名がないため、"{基底クラス名}"として記録しています。これにより、基底クラスの定義順序変更にも対応できます。

3-2.参照を経由する場合

ポリモーフィックな基底クラスへの参照をシリアライズすることで、ポリモーフィックに回復されます。つまり、派生クラスのインスタンスを基底クラス型の参照でポイントして保存し回復した場合、派生クラスのインスタンスとして回復されます。


3.オブジェクト追跡の仕組み

3-1.ポインタをシリアライズする仕組み

Theolizerはboost::serializationと同様にポインタをファイルへ保存し、そのファイルを回復した時、元のポイント構造を回復できる仕組みを実装しています。

下記のようなオブジェクト追跡処理により実現しています。

・ポインタの保存

  1. オブジェクトIDテーブル
    インスタンスを保存する時に、インスタンスの先頭アドレスに対してオブジェクトIDを割り当て、インスタンスの先頭アドレスとオブジェクトIDの対応表を生成します。
  2. ポインタを保存
    ポインタの指すアドレスに対応するオブジェクトIDをオブジェクトIDテーブルで求め、オブジェクトIDを保存します。

・ポインタの回復

  1. オブジェクトIDテーブル
    インスタンスを回復する時に、オブジェクトIDに対応するインスタンスの先頭アドレスを記録します。
  2. ポインタを回復
    オブジェクトIDを読み出し、それに対応するインスタンスの先頭アドレスをオブジェクトIDテーブルで求め、インスタンスの先頭アドレスをポインタに設定します。

インスタンスは任意の「型」のインスタンスですが、これが構造体の場合、構造体全体の先頭アドレスと構造体の先頭メンバの先頭アドレスは一致します。(クラスの場合も同様です。)これらを纏めて1つのオブジェクトIDとすると適切に回復できないため、インスタンスの先頭アドレスだけでなく「型」も一緒に記録し、アドレスと型に対してオブジェクトIDを割り当てています。


3-2.オブジェクト追跡の課題

ポインタを保存する前に必ずそのポインタが指すインスタンスを保存できれば特に問題はないのですが、そうとは限りません。

ポインタの指すインスタンスが例えばローカル変数で、それがポインタより後でシリアライズ指示される場合もあります。
そのようなデータ構造を回復するためには、そのローカル変数の保存は最初にポインタをシリアライズ指示された時ではなく、ローカル変数のシリアライズ指示まで遅らせるべきです。そうしないと順序よく回復できないため回復手順が複雑で負荷が高いものになります。

しかし、ポインタが指している領域の所有権を当該ポインタが持っている場合は、最初にポインタをシリアライズ指示された時にインスタンスを保存すればOKです。最初にポインタを回復する際にインスタンス領域を確保して回復すれば適切にポイント構造を回復できますから。(*1)

従って、ポインタがポイント先の所有権を持っているかどうかを判定できれば適切に保存/回復できるのですが、残念ながらシリアライズしようとしているポインタが所有権のある領域を指しているのか、そうでない領域を指しているのか判定する仕組みはC++言語仕様にはありません。

そこで、下記2つの選択肢を検討しました。

  1. boost::serializationのようにこのようなケースをエラーとする(*2)
    この場合ローカル変数のようなインスタンスはそれをポイントするポインタより先に保存する必要が有ります。できれば避けたい制約と思います。
  2. ポインタの属性として所有権の有無をプログラマが指定する 保存順序の制約はなく、処理負荷も特に高くはなりません。その代わりプログラマに少し負担がかかります。

さて、インスタンスの所有権を持つか持たないかをポインタの属性とすることはリソース・リークし難いプログラムになりますのでたいへん好ましいと考えます。実際、C++11のスマート・ポインタは正にそのような概念に基づいています。スマート・ポインタが管理する領域はスマート・ポインタが所有権を持ち、その他のポインタは所有権を持ちません。所有権が明確ですのでメモリ・リークのリスクを大きく低減できます。

そこで、インスタンスの所有権の有無をポインタの属性とする仕組みを実装することで、この問題を回避することにしました。

(*1)ポインタが指すオブジェクトをオブジェクト追跡単位の最後で保存する案も検討しました
途中でボイントされているローカル変数がシリアライズされたら、その時点で保存すれば良いです。制約も少なくプログラマへの負荷も小さいように見えます。
しかし、最後までポイント先のインスタンスがシリアライズされなかった時、ユーザ・プログラムにおけるシリアライズ漏れ不具合なのか、所有権のある領域なのかの判断がつきません。前者の不具合は発見が遅れるとかなり痛いですので、この案は棄却しました。
(*2)boostの場合
boost::serializationはこのような使い方を許可していません。上記ケースではローカル変数を保存する時にpointer_conflict例外が投げられます。
"Pointer Conflict" Errors in the Boost Serialization Library
Boost serialization with pointers


3-3.オブジェクト追跡で用いる用語

このために、少し用語を定義しました。

  1. 静的定義領域
    ポインタが所有権(獲得/解放する権限)を持つことができない領域です。下記があります。
    • グローバル変数
    • ローカル変数
    • 他のクラスのメンバ変数(非ボインタ) これは、例えばstruct Foo { int mInt; };のmIntです。ポインタ側からmIntを獲得/解放することができません。
  2. 動的生成領域(*3)
    newやnew[]で獲得するインスタンスです。
  3. ポインタ型
    通常のポインタです。これはポイント先のインスタンスの所有権を持ちません。
    静的定義領域、動的生成領域の両方をボイントすることができませ。
  4. オーナー・ポインタ型
    ポイント先のインスタンスの所有権を持っているポインタです。
    当然ですが動的生成領域のみポイント可能です。
(*3)「静的定義領域」と言う名称
以前、teratailで「インスタンス生成方法の呼び方について」質問し、catsforepawさんの回答を元に決定しました。
遠いところからですが、catsforepawさん、その節はありがとうございました。


3-4.オブジェクト追跡対象について

静的定義領域は、全てのシリアライズ対象の変数がオブジェクト追跡候補になります。しかし、実際にポインタでポイントされる変数はその内の一部だけですので、全てをオブジェクト追跡するのは無駄が多いです。そこで、2-1.オブジェクト追跡する領域について で示した方法でオブジェクト追跡対象を絞り込んでいます。


3-5.オブジェクト追跡単位について

2-2.オブジェクト追跡単位について に示したオブジェクト追跡単位について少し詳しく説明します。

これはオブジェクトIDテーブルの有効期間です。clearTracking()することで

  1. オブジェクトIDテーブルにシリアライズされていないインスタンスが登録されていないか確認します
  2. オブジェクトIDテーブルをクリアします

前者により、ユーザ・プログラムのバグ検出をサポートします。
後者により、不要なオブジェクト追跡を解除できるのでバフォーマンスを改善できます。

注意事項1:
解放したインスタンスを追跡したままにしていると未定義メモリ・アクセスが発生する場合があります。ですので、clearTracking()するまで、もしくは、シリアライザ自身を破棄するまで下記インスタンスを破棄しないで下さい。
・シリアライズしたインスタンス
・シリアライズしたポインタが指すインスタンス
なお、clearTracking()することで「シリアライズした」と言う記録が全て破棄されますので、必要であれば、cleartTracking()後、最初のシリアライズまでの間ならばインスタンスを安全に破棄できます。
注意事項2:
clearTracking()により、未解決なオブジェクト追跡(ポインタが登録されたがポインタの指すインスタンスが保存/回復されていない)の有無を確認します。もし、未解決なオブジェクト追跡がある時、WrongUsingエラーを通知します。(「エラー報告 」参照)
なお、オブジェクト追跡を行い、かつ、clearTracking()を呼び出していない場合、シリアライザ・インスタンスのデストラクタでプログラムを異常終了させています。オブジェクト追跡する場合は必ずデストラクトする前にclearTracking()を呼び出すようにして下さい。


4.オブジェクト追跡の網羅的な使用例(自動テスト)の説明

4-1.各種メモリへのポインタのテスト

4-1-1.概要

ここでは、C++で用いられる少し異なるメモリ上のインスタンスへのポインタが適切に回復されることをテストしています。具体的には下記の通りです。

  1. グローバル変数
    ここには静的定義領域のみ存在できますので、ここへのポインタをオーナー指定できません。
  2. ローカル変数(スタック変数)
    ここも静的定義領域のみ存在できますので、ここへのポインタをオーナー指定できません。
  3. ヒープ変数
    ここには動的生成領域と静的定義領域の両方が存在できます。newで獲得したクラスのインスタンス全体は動的生成領域です。そのクラスの各メンバ変数は静的定義領域となります。基底クラスは少し複雑です。
    • ポリモーフィックな(仮想関数がある)クラスの場合
      基底クラスへのポインタ経由でdeleteできますので、基底クラスへのポインタをオーナー指定できます。

    • 非ポリモーフィックな(仮想関数がない)クラスの場合
      C++仕様上、基底クラスへのポインタ経由でdeleteすると派生クラスのデストラクタは呼ばれず基底クラスとしてデストラクトされます。従って、非ポリモーフィックなクラスの場合、その基底クラスへのポインタをオーナー指定してはいけません。

下記組み合わせのインスタンスとポインタ(非オーナー)について保存/回復テストを行います。

静的定義領域(グローバル変数, ローカル変数, メンバ変数) + 動的生成領域(プリミティブ, クラス)
                            ×
Pointee指定プリミティブ型(long) + Pointee指定なしクラス型(ObjectTracking0) + Pointee指定クラス型(ObjectTracking1)

また、2番目以降の基底クラス・ポインタは一般にインスタンス先頭ではないアドレスを指しますが、このケースにも対応できていることを確認します。 そのために、派生クラスのインスタンス、および、その2番目の基底クラスへのポインタをシリアライズし、ポインタが回復したインスンタスをポイントすることを確認しています。

更に、動的生成領域(クラス)へのポインタ(非オーナー)のテストを実施する際に、std::shared_ptr<>を非侵入型手動対応しました。その時、std::shared_ptr<>対応は意外に難しいことが分かり、いくつか複雑な対応を行いましたので、ここでその一部のテストも行っています。(後日、標準コンテナ対応する予定です。その時、網羅的な自動テストを実装します。)

4-1-2.ソース・コード

source/reference_and_test/basic2/test_object_tracking.hでテスト用のクラスを定義してます。

  1. 2番目以降の基底クラスへのポインタの回復テスト用
    • ObjectTrackingBase0 1番目の基底クラス
    • ObjectTrackingBase1 2番目の基底クラス
    • ObjectTrackingDerived 上記2つを継承したクラス

  2. 各種メモリへのポインタのテスト用
    • ObjectTracking0 被ポイント指定(Pointee)なし
    • ObjectTracking1 被ポイント指定(Pointee)あり
    • StaticDefinition メンバ変数静的定義領域用
    • Pointers 自動シリアライズするポインタの定義

  3. 同じインスタンスを複数回シリアライズした時のテスト用 上記で定義したObjectTrackingDerivedを用います。

source/reference_and_test/basic2/test_object_tracking2.cppでテスト関数を定義してます。

  1. 保存処理
    template<class tSerializer>
    void saveObjectTracking(tSerializer& iSerializer)の前半

  2. 回復処理
    template<class tSerializer>
    void loadObjectTracking(tSerializer& iSerializer)の前半

4-2.ポインタ(非オーナー)のテスト

4-2-1.概要

ポインタのシリアライズ、および、ポイント先のインスタンスのシリアライズについて、指定方法が手動(トップ・レベル)、自動シリアライズ(自動型のメンバ変数)、手動(非トップ・レベル)の3種類あります。それぞれについて各種の型、および、ポインタ→インスタンスとインスタンス→ポインタの順序おのおのの組み合わせのテストを行います。

ポインタ:
 手動(トップ・レベル) + 自動シリアライズ(自動型のメンバ変数) + 手動(非トップ・レベル)
            ×
インスタンス:
 手動(トップ・レベル) + 自動シリアライズ(自動型のメンバ変数) + 手動(非トップ・レベル)
            ×
全てのプリミティブ + enum型 + scoped num型 + クラス + 各配列型
            ×
ポインタを先に保存 + インスタンスを先に保存

4-2-2.ソース・コード

source/reference_and_test/basic2/test_object_tracking.hでテスト用のクラスとマクロを定義してます。

  1. DEFINE_MEMBERS()マクロ
    各種の型に対応する、変数宣言や初期化を定義するためのマクロです。5-1.単独テスト と同じ名前ですが、少し異なるマクロです。定義している変数の型は同じですが、初期化値が異なります。

  2. PointeeListクラス
    静的定義領域の定義です。各型のインスタンスについて、自動シリアライズと手動(トップ・レベル)を担います。前者は非侵入型完全自動として処理し、後者はsavePointee()とloadPointee()関数で行います。

  3. PointeeListManualクラス
    静的定義領域の定義です。各型のインスタンスについて、手動(非トップ・レベル)を担います。

  4. PointerListクラス
    ポインタ(非オーナー)の定義です。各型のポインタについて、自動シリアライズと手動(トップ・レベル)を担います。前者は非侵入型完全自動として処理し、後者はsavePointer()とloadPointer()関数で行います。トップ・レベル、および、自動型については、例えばデバッグやログ用にconst領域へのポインタの保存を想定して、constポインタも定義しています。

  5. PointerListManual
    ポインタ(非オーナー)の定義です。各型のポインタについて、手動(非トップ・レベル)を担います。回復処理を行わない手動型の実装は想定不要と考えますので、constポインタを定義していません。

source/reference_and_test/basic2/test_object_tracking2.cppでテスト関数を定義してます。

  1. 保存処理
    template<class tSerializer>
    void saveObjectTracking(tSerializer& iSerializer)の後半

  2. 回復処理
    template<class tSerializer>
    void loadObjectTracking(tSerializer& iSerializer)の後半

4-3.オーナー・ポインタのテスト

4-3-1.概要

オーナー・ポインタのシリアライズについて、指定方法が手動(トップ・レベル)、自動シリアライズ(自動型のメンバ変数)、手動(非トップ・レベル)の3種類あります。それぞれについて各種の型の組み合わせのテストを行います。また、nullptrの回復もテストします。
(ポインタ(非オーナー)と異なり、インスタンス側はポインタが管理するため組み合わせが発生しません。また、インスタンスの保存は先に出現した方が先ですので、保存順序の組み合わせも発生しません。)

オーナー・ポインタ:
 手動(トップ・レベル) + 自動シリアライズ(自動型のメンバ変数) + 手動(非トップ・レベル)
            ×
全てのプリミティブ + enum型 + scoped num型 + クラス + 各配列型

4-3-2.ソース・コード

source/reference_and_test/basic2/test_object_tracking.hでテスト用のクラスとマクロを定義してます。

  1. DEFINE_MEMBERS()マクロ
    ポインタ(非オーナー)と共通です。

  2. NEW_ARRAY()マクロ
    C++は、配列型(Type[N])をnewした結果は残念なことに配列型へのポインタ(Type(*)[N])にならず要素へのポインタ(Type*)になります。そのため、配列型へのポインタ(Type(*)[N])変数へそのままでは代入できません。これをreinterpret_cast<Type(*)[N]>()して(*4)代入できるようにするマクロです。

  3. OwnerListクラス
    オーナー・ポインタの定義です。各型のポインタについて、自動シリアライズと手動(トップ・レベル)を担います。前者は非侵入型完全自動として処理し、後者はsavePointer()とloadPointer()関数で行います。インスタンスを回復しないオーナー指定ポインタは意味がないので、constポインタは定義していません。

  4. OwnerListManualクラス
    オーナー・ポインタの定義です。各型のポインタについて、手動(非トップ・レベル)を担います。OwnerListクラスと同様constポインタを定義していません。

(*4)「配列型へのポインタ」のサポート
以前、teratailで「配列型(サイズ含む)へのポインタをnewで生成したい」旨を質問してraccyさんの回答をヒントに対応できました。
遠いところからですが、raccyさん、ありがとうございました。

source/reference_and_test/basic2/test_object_tracking3.cppでテスト関数を定義してます。

  1. 保存処理
    template<class tSerializer>
    void saveObjectTracking3(tSerializer& iSerializer)の前半

  2. 回復処理
    template<class tSerializer>
    void loadObjectTracking3(tSerializer& iSerializer)の前半

4-4.同じインスタンスを複数回シリアライズするテスト

ObjectTrackingDerivedのインスタンスに対して、被ポインタ指定したものとしていないものについて保存/回復テストを行います。

source/reference_and_test/basic2/test_object_tracking3.cppでテスト関数を定義してます。

  1. 保存処理
    template<class tSerializer>
    void saveObjectTracking3(tSerializer& iSerializer)の後半

  2. 回復処理
    template<class tSerializer>
    void loadObjectTracking3(tSerializer& iSerializer)の後半

また、同じインスタンスを複数回保存し、それを異なるインスタンスへ回復しようとした時、WrongUsing例外が発生することのテストを行います。

source/reference_and_test/basic2/test_object_tracking.cppでテスト関数を定義してます。

tutoriseObjectTracking()関数の最後の方、「複数回シリアライズ・データの回復エラーテスト」で行っています。


5.ポリモーフィズムの網羅的な使用例(自動テスト)の説明

ここでは、基底クラスへのポインタでポイントされる異なる派生クラスを適切に回復できることを確認します。
非侵入型完全自動、侵入型半自動、非侵入型手動の3種類の基底クラスと派生クラスを用意し、派生クラスは3種類の基底クラスを全て継承しました。

source/reference_and_test/basic2/test_polymorphism.hでテスト用のクラスを定義してます。

  1. 基底クラス
    • PolyBaseFullAuto 非侵入型完全自動
    • PolyBaseHalfAuto 侵入型半自動
    • PolyBaseManual 非侵入型手動

  2. 派生クラス
    • PolyDerivedFullAuto 非侵入型完全自動
    • PolyDerivedHalfAuto 侵入型半自動
    • PolyDerivedManual 非侵入型手動

source/reference_and_test/basic2/test_polymorphism.cppでテスト関数を定義してます。

  1. 保存処理
    template<class tSerializer>
    void savePolymorphism(tSerializer& iSerializer)

  2. 回復処理
    template<class tSerializer>
    void loadPolymorphism(tSerializer& iSerializer)

基底クラスのインスタンスをstd::unique_ptr<>で保持し、そのstd::unique_ptr<>のリストをstd::vector<>で保持しています。(std::vector<>とstd::unique_ptr<>を非侵入型手動クラスとして仮にシリアライズ対応しています。)
3種類の基底クラス毎にstd::vector<>を用意しているので、std::vector<>も下記の3種類あります。

  • 非侵入型完全自動
  • 侵入型半自動
  • 非侵入型手動

各std::vector<>には3種類の派生クラスのインスタンスを登録して、std::vector<>を保存します。そして、全てのポインタをnullptr設定して回復し、保存した時の派生クラスを回復できたことを確認しています。