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) { }
MSGPACK_DEFINE(base_);
private:
std::shared_ptr<Monster> base_;
};
std::shared_ptr<Goblin> pder = std::make_shared<Goblin>();
CaughtMonster obj(pder);
std::stringstream strm;
msgpack::pack(strm, obj);
ポリモーフィックなポインタは実体でシリアライズ/デシリアライズしてほしいよなぁ・・ってことで、ちょっと工夫をしてみました.
MessagePackにはライブラリ側で定義されたAdapterがいろいろあって、何もしなくてもshared_ptrのシリアライズ可能になっています.
そこで、「ある特定クラスの派生クラスだけに限定して使われる shared_ptr アダプタ」を定義して、派生クラスの情報を含めたシリアライズ/デシリアライズが行えるようにする実装をしてみます.
まず、ポリモーフィック型でのシリアライズ/デシリアライズに必要なメソッドを定義する抽象クラスを定義します.
このクラスの派生クラスであることをポリモーフィックシリアライズの条件にします.
#include <msgpack.hpp>
class poly_pack_base {
protected:
poly_pack_base() noexcept = default;
public:
virtual ~poly_pack_base() = default;
virtual int get_type_id() const noexcept = 0;
virtual void read_object_v(msgpack::object const& o) = 0;
virtual void write_object_v(msgpack::object::with_zone& oz) const = 0;
};
次に、多態シリアライズ対象となる基底クラスの実装です.
poly_pack_baseとして要求される純粋仮想関数をオーバーライドして、このクラスとこのクラスから派生するクラスのオブジェクトを復元するためのファクトリクラス型を公開します.
このファクトリクラスはこのクラスとこのクラスの派生クラスから共通して参照され、各々の型が持つTYPE_IDで復元すべき型を指定してインスタンスを生成します.
ファクトリ関数がオブジェクトを生成するときは、特別な情報を必要とせずにオブジェクトを生成できるようにしておく必要があるので、忘れずに引数のないデフォルトコンストラクタを定義しておきます.
#include "restore_factory.h"
#include "poly_pack_base.h"
#include <msgpack.hpp>
class Monster : public poly_pack_base {
public:
Monster() noexcept;
:
using factory_race_class_t = restore_factory;
constexpr int TYPE_ID() const noexcept
{
return static_cast<int>(PackClassIDs::Mid_Monster);
}
virtual int get_type_id() const noexcept override
{
return TYPEID();
}
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));
}
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_;
};
基底クラスから派生するクラスです.
実装はほぼ同様ですが、ファクトリクラスの公開は不要です.
#include "Monster.h"
#include <msgpack.hpp>
class Goblin : public Monster {
public:
Goblin() noexcept;
:
:
constexpr int TYPE_ID() const noexcept
{
return static_cast<int>(PackClassIDs::Mid_Goblin);
}
virtual int get_type_id() const noexcept override
{
return TYPE_ID();
}
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));
}
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ファイルは以下のような感じです.
class Monster;
enum class PackClassIDs : int {
Mid_Monster,
Mid_Goblin,
};
class restore_factory {
public:
restore_factory() = delte;
~RestorationFactory() = delete;
static std::shared_ptr<Monster> CreateObject(int pack_id);
};
#include "restore_factory.h"
#include "Monster.h"
#include "Goblin.h"
std::shared_ptr<Monster> restore_factory::CreateObject(int pack_id)
{
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を使って対象を制限します.
#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>
namespace msgpack {
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
namespace adaptor {
template <typename T>
struct pack<std::shared_ptr<T>,
std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
template <typename Stream>
msgpack::packer<Stream>& operator()(msgpack::packer<Stream>& o,
const std::shared_ptr<T>& v) const
{
if (v) {
o.pack_array(2);
int pack_id = v->get_type_id();
o.pack(pack_id);
msgpack::zone z;
msgpack::object::with_zone oz(z);
v->write_object_v(oz);
o.pack(static_cast<msgpack::object&>(oz));
}
else {
o.pack_nil();
}
return o;
}
};
template <typename T>
struct object_with_zone<std::shared_ptr<T>,
std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
void operator()(msgpack::object::with_zone& oz,
const std::shared_ptr<T>& v) const
{
if (v) {
msgpack::zone& zone = oz.zone;
oz.type = msgpack::type::ARRAY;
oz.via.array.size = 2;
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);
int pack_id = v->get_type_id();
oz.via.array.ptr[0] = msgpack::object(pack_id);
msgpack::object::with_zone oz_mine(zone);
v->write_object_v(oz_mine);
oz.via.array.ptr[1] = oz_mine;
}
else {
oz.type = msgpack::type::NIL;
}
}
};
template <typename T>
struct convert<std::shared_ptr<T>,
std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
msgpack::object const& operator()(msgpack::object const& o,
std::shared_ptr<T>& v) const
{
switch (o.type) {
case msgpack::type::NIL:
break;
case msgpack::type::ARRAY:
if (o.via.array.size == 2) {
using factory_t = typename T::factory_race_class_t;
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;
}
};
};
};
};
Step.6 マクロ化しちゃえばすっきり
よーくみると基底クラスと派生クラスはほとんと同じ実装なので、マクロ化するとスッキリしますね.
マクロはこんな感じでしょうか.
#include <msgpack.hpp>
class poly_pack_base {
protected:
poly_pack_base() noexcept = default;
public:
virtual ~poly_pack_base() = default;
virtual int get_type_id() const noexcept = 0;
virtual void read_object_v(msgpack::object const& o) = 0;
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;
#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)); \
}
基底クラスの定義.
ずいぶんとスッキリしました.
#include "restore_factory.h"
#include "poly_pack_base.h"
#include "poly_serializeable_adapter.h"
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_;
};
こちらは派生クラスの定義です.
#include "Monster.h"
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_;
};
多態性を維持したままのシリアライズができれば、だいたいのシーンには対応できそうですね.
参考記事