ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

C++でMessagePackを使ってみる(6)〜 Polymorphism(多態性)とシリアライズ 〜

多態性キープしてシリアライズしたいけど・・

MessagePackを使うとかなり簡単にC++でデータのシリアライズ/シリアライズが実現できるのですが、いろいろ使い込んでいくと一つ困った状況に気づきます.
ポインタで表現されるオブジェクト( 生ポインタではなく、std::shared_ptr )をシリアライズ/デシリアライズする時、ポインタの型でシリアライズ/デシリアライズしようとしてしまい、ポインタが指しているオブジェクトの実体の型でシリアライズ/デシリアライズが出来ないという問題です.

class Monster {
    int life_point_;
    int magic_point_;
};

class Goblin : public Monster {
    int race_class_;
    int drop_item_ids_; 
};

というクラスがあったとして、このような場合です.

class CaughtMonster {
    CaughtMonster(std::shared_ptr<Monster> dep)
    : base_(dep) { }

    // Goblinでシリアライズ/デシリアライズされてほしいけれども....
    MSGPACK_DEFINE(base_);
private:
    std::shared_ptr<Monster> base_;
};

std::shared_ptr<Goblin> pder = std::make_shared<Goblin>();
CaughtMonster obj(pder);


// base_は、GoblinではなくMonsterとしてシリアライズされる.
// この結果、race_class_, drop_item_ids_の情報がシリアライズ時に失われてしまう.
std::stringstream strm;
msgpack::pack(strm, obj);

ポリモーフィックなポインタは実体でシリアライズ/デシリアライズしてほしいよなぁ・・ってことで、ちょっと工夫をしてみました.

ポリモーフィックシリアライズ対応

MessagePackにはライブラリ側で定義されたAdapterがいろいろあって、何もしなくてもshared_ptrシリアライズ可能になっています.
そこで、「ある特定クラスの派生クラスだけに限定して使われる shared_ptr アダプタ」を定義して、派生クラスの情報を含めたシリアライズ/デシリアライズが行えるようにする実装をしてみます.

Step.1 ポリモーフィックシリアライズ対応を示すクラスを定義する

まず、ポリモーフィック型でのシリアライズ/デシリアライズに必要なメソッドを定義する抽象クラスを定義します.
このクラスの派生クラスであることをポリモーフィックシリアライズの条件にします.

//
// poly_pack_base.h
//
#include <msgpack.hpp>

// 多態シリアライズ対象クラスの基底クラス
class poly_pack_base {
protected:
    poly_pack_base() noexcept = default;
public:
    virtual ~poly_pack_base() = default;

    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept = 0;
    // 読み出し仮想関数
    virtual void read_object_v(msgpack::object const& o) = 0;
    // with_zoneオブジェクトへ書き込む仮想関数
    virtual void write_object_v(msgpack::object::with_zone& oz) const = 0;
};

Step.2 ポリモーフィックシリアライズ対応の基底クラスを準備する

次に、多態シリアライズ対象となる基底クラスの実装です.

poly_pack_baseとして要求される純粋仮想関数をオーバーライドして、このクラスとこのクラスから派生するクラスのオブジェクトを復元するためのファクトリクラス型を公開します.
このファクトリクラスはこのクラスとこのクラスの派生クラスから共通して参照され、各々の型が持つTYPE_IDで復元すべき型を指定してインスタンスを生成します.
ファクトリ関数がオブジェクトを生成するときは、特別な情報を必要とせずにオブジェクトを生成できるようにしておく必要があるので、忘れずに引数のないデフォルトコンストラクタを定義しておきます.

//
// Monster.h
//

// オブジェクトを復元するファクトリクラスのヘッダ
#include "restore_factory.h"
#include "poly_pack_base.h"
#include <msgpack.hpp>

// Monsterクラス
class Monster : public poly_pack_base {
public:
    // デフォルトコンストラクタ. 
    // ファクトリがインスタンスを作るために必要.
    Monster() noexcept;
         :
    // カスタムアダプタから参照するためのファクトリクラスのtype
    using factory_race_class_t = restore_factory;

    // クラス型を示すID
    constexpr int TYPE_ID() const noexcept
    {
        return static_cast<int>(PackClassIDs::Mid_Monster);
    }
    // ------------------------------------------------
    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept override
    {
        return TYPEID();
    }

    // ------------------------------------------------
    // msgpack::objectからオブジェクトを読み出す.
    virtual void read_object_v(msgpack::object const& o) override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator>>(o, static_cast<self_t&>(*this));
    }
    // -----------------------------------------------
    // with_zoneオブジェクトにこのクラスのシリアライズデータを書き込む.
    virtual void write_object_v(msgpack::object::with_zone& oz) const override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator<<(oz, static_cast<self_t&>(*this));
    }

    // 通常のシリアライズ定義.
    MSGPACK_DEFINE(life_point_, magic_point_);
private:
    int life_point_;
    int magic_point_;
};

Step.3 ポリモーフィックシリアライズ対応の派生クラスを準備する

基底クラスから派生するクラスです.
実装はほぼ同様ですが、ファクトリクラスの公開は不要です.

//
// Goblin.h
//
#include "Monster.h"
#include <msgpack.hpp>

// Goblinクラス
class Goblin : public Monster {
public:
    // デフォルトコンストラクタ.  復元時に必要になる.
    Goblin() noexcept;
           :
           :
    // ### 実体クラスを示すID定数
    constexpr int TYPE_ID() const noexcept
    {
        return static_cast<int>(PackClassIDs::Mid_Goblin);
    }
    // ------------------------------------------------
    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept override
    {
        return TYPE_ID();
    }

    // ------------------------------------------------
    // msgpack::objectからオブジェクトを読み出す.
    virtual void read_object_v(msgpack::object const& o) override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator>>(o, static_cast<self_t&>(*this));
    }
    // -----------------------------------------------
    // with_zoneオブジェクトにこのクラスのシリアライズデータを書き込む.
    virtual void write_object_v(msgpack::object::with_zone& oz) const override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator<<(oz, static_cast<self_t&>(*this));
    }

    MSGPACK_DEFINE(MSGPACK_BASE(Monster), race_class_, drop_item_ids_);
private:
    int race_class_;
    int drop_item_ids_;
};

Step.4 IDに対応するファクトリクラスを実装する

ファクトリクラスは、クラスのもとの型を示すTYPE_IDからもとのオブジェクトを生成するクラスです.
idに応じたオブジェクトを生成して返します.
headerファイルとsourceファイルは以下のような感じです.

//
// restore_factory.h
//

// Fwd Decl
class Monster;

// --------------------------------------------------------------
// Factoryが対象とするクラスのTYPE_ID定義用enum
enum class PackClassIDs : int {
    Mid_Monster, // Monsterクラス
    Mid_Goblin,  // Goblinクラス
};

// --------------------------------------------------------------
// ファクトリクラス
class restore_factory {
public:
    restore_factory() = delte;
    ~RestorationFactory() = delete;
    // pack_id から 元の型のインスタンスを生成
    static std::shared_ptr<Monster> CreateObject(int pack_id);
};
//
// restore_factory.cpp
//
#include "restore_factory.h"
#include "Monster.h"
#include "Goblin.h"

// --------------------------------------------------------------
// ファクトリ関数
std::shared_ptr<Monster> restore_factory::CreateObject(int pack_id)
{
    // 基底クラスのshared_ptr型で返す
    std::shared_ptr<Monster> ptr;
    switch (pack_id) {
    case Monster::TYPE_ID():
        ptr = std::make_shared<Monster>();
        break;
    case Goblin::TYPE_ID():
        ptr = std::make_shared<Goblin>();
        break;
    default:
        break;
    }
    return ptr;
}

Step.5 ポリモーフィック対応したアダプタを実装する

最後に convert, pack, object_with_zone カスタムアダプタを定義します.
ただし、poly_pack_base の派生クラスであることを条件にしてアダプタが適用されるように、 各アダプタの第2テンプレートパラメータでenable_ifを使って対象を制限します.

//
// poly_serializeable_adapter.h
//
#include "poly_pack_base.h"
#include <msgpack.hpp>
#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>

// message packの名前空間
namespace msgpack {
// version 名前空間
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
// message packのアダプタ用名前空間
namespace adaptor {
// -------------------------------------------------------------------------
// pack アダプタ
template <typename T>
struct pack<std::shared_ptr<T>,
            std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
    // pack
    template <typename Stream>
    msgpack::packer<Stream>& operator()(msgpack::packer<Stream>& o,
                                        const std::shared_ptr<T>& v) const
    {
        // 有効なポインタ
        if (v) {
            // 2要素のarrayとしてpackする
            // array[0] <- TYPE_ID
            // array[1] <- 本体のobject
            o.pack_array(2);
            // 実際の型のTYPE_ID取得
            int pack_id = v->get_type_id();
            // TYPE_IDを書き込み
            o.pack(pack_id);
            // zoneとwith_zone生成
            msgpack::zone z;
            msgpack::object::with_zone oz(z);
            // 仮想関数でポインタのデータを中間形式with_zoneへ書き込む
            v->write_object_v(oz);
            // ozを書き込み.
            o.pack(static_cast<msgpack::object&>(oz));
        }
        else {
            // nullptrの場合, nullとしてpack
            o.pack_nil();
        }
        return o;
    }
};

// -------------------------------------------------------------------------
// object_with_zone アダプタ シリアライズ用
template <typename T>
struct object_with_zone<std::shared_ptr<T>,
                        std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
    // oへvを書き込む
    void operator()(msgpack::object::with_zone& oz,
                    const std::shared_ptr<T>& v) const
    {
        if (v) {
            // zoneの参照を取り出す.
            msgpack::zone& zone = oz.zone;
            // oのタイプをサイズ2のArrayに設定
            oz.type = msgpack::type::ARRAY;
            oz.via.array.size = 2;
            // object2つ分の領域をzone上に確保
            void* pmem = zone.allocate_align(sizeof(msgpack::object) * oz.via.array.size,
                                             MSGPACK_ZONE_ALIGNOF(msgpack::object));
            oz.via.array.ptr = static_cast<msgpack::object*>(pmem);

            // pack_idを取得して書き込む
            int pack_id = v->get_type_id();
            oz.via.array.ptr[0] = msgpack::object(pack_id);

            // ポインタ実体のオブジェクト書き込み
            // 実体オブジェクト用のwith_zone(msgpack::objectの派生クラス)を準備して
            // vの仮想関数でv自身をwith_zoneへ書き込ませる.
            msgpack::object::with_zone oz_mine(zone);
            v->write_object_v(oz_mine);
            oz.via.array.ptr[1] = oz_mine;
        }
        else {
            // v == nullptr
            // oのタイプをnilに設定.
            oz.type = msgpack::type::NIL;
        }
    }
};

// -------------------------------------------------------------------------
// デシリアライズ用の convertアダプタ
template <typename T>
struct convert<std::shared_ptr<T>,
               std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
    // oから読み出してvへオブジェクトを復元
    msgpack::object const& operator()(msgpack::object const& o,
                                      std::shared_ptr<T>& v) const
    {
        switch (o.type) {
        case msgpack::type::NIL:
            // nullptrなので何もせず.
            break;
        case msgpack::type::ARRAY:
            // サイズが2であることを確認
            if (o.via.array.size == 2) {
                // Tもしくは派生クラスであればfactory_race_class_tが参照できるはず.
                using factory_t = typename T::factory_race_class_t;
                // 元の型のTYPE_ID読み出し
                // factoryを使って元の型のオブジェクトを生成
                // もとのオブジェクトの仮想関数を使ってobjからデータを読み出す
                int pack_id = o.via.array.ptr[0].as<int>();
                v = std::static_pointer_cast<T>(factory_t::CreateObject(pack_id));
                v->read_object_v(o.via.array.ptr[1]);
            }
            else {
                // サイズのミスマッチ
                // ミスマッチの例外を投げる
                throw type_err();
            }
            break;
        default:
            // 型のミスマッチ
            // ミスマッチの例外を投げる
            throw type_err();
        }
        return o;
    }
};

}; // adaptor
}; // v3
}; // msgpack

Step.6 マクロ化しちゃえばすっきり

よーくみると基底クラスと派生クラスはほとんと同じ実装なので、マクロ化するとスッキリしますね.
マクロはこんな感じでしょうか.

//
// poly_pack_base.h
//
#include <msgpack.hpp>

// Polymorphicシリアライズ対象クラスの基底クラス
class poly_pack_base {
protected:
    poly_pack_base() noexcept = default;
public:
    virtual ~poly_pack_base() = default;

    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept = 0;
    // with_zoneオブジェクトからオブジェクトを読み出す.
    virtual void read_object_v(msgpack::object const& o) = 0;
    // with_zoneオブジェクトにこのクラスのシリアライズデータを書き込む.
    virtual void write_object_v(msgpack::object::with_zone& oz) = 0;
};

// レストア用ファクトリ宣言
#define MSGPACK_EXTRA_DECL_FACTORY_CLASS( factory_name ) \
    using factory_race_class_t = factory_name;

// pack_id定義
#define MSGPACK_EXTRA_DECL_TYPE_ID( pack_id ) \
    constexpr int TYPE_ID() const noexcept \
    { return static_cast<int>(pack_id); } \
    virtual int get_type_id() const noexcept override \
    { return TYPE_ID(); }

// 仮想関数実装
#define MSGPACK_EXTRA_POLYPACK_METHOD_IMPL() \
    virtual void read_object_v(msgpack::object const& mpkobj) override { \
        using self_t = std::remove_reference_t<decltype(*this)>; \
        msgpack::operator>>(mpkobj, static_cast<self_t&>(*this)); \
    } \
    virtual void write_object_v(msgpack::object::with_zone& mpkobjz) const override { \
        using self_t = std::remove_reference_t<decltype(*this)>; \
        msgpack::operator<<(mpkobjz, static_cast<self_t&>(*this)); \
    }

基底クラスの定義.
ずいぶんとスッキリしました.

//
// Monster.h
//

// オブジェクトを復元するファクトリクラスのヘッダ
#include "restore_factory.h"
#include "poly_pack_base.h"
#include "poly_serializeable_adapter.h"

// Monsterクラス
class Monster : public poly_pack_base {
public:
    Monster() noexcept;
         :
    MSGPACK_EXTRA_DECL_FACTORY_CLASS(restore_factory);
    MSGPACK_EXTRA_DECL_TYPE_ID(MosterClassTypeIds::Mid_Monster);
    MSGPACK_EXTRA_POLYPACK_METHOD_IMPL();

    MSGPACK_DEFINE(life_point_, magic_point_);
private:
    int life_point_;
    int magic_point_;
};

こちらは派生クラスの定義です.

//
// Goblin.h
//
#include "Monster.h"

// Goblinクラス
class Goblin : public Monster {
public:
    Goblin() noexcept;
        :
    MSGPACK_EXTRA_DECL_TYPE_ID(PackClassIDs::Mid_Goblin);
    MSGPACK_EXTRA_POLYPACK_METHOD_IMPL();

    MSGPACK_DEFINE(MSGPACK_BASE(Monster), race_class_, race_class_);
private:
    int race_class_;
    int drop_item_ids_;
};

多態性を維持したままのシリアライズができれば、だいたいのシーンには対応できそうですね.


参考記事