メモ:cerealと派生クラス

メモです。cerealは説明不要のシリアライズ用ライブラリ。

派生クラスをstd::unique_ptr<Base>として持っている時の最小サンプル。

  • cereal::base_class<Base>(this)シリアライズするのを忘れない
  • CEREAL_REGISTER_TYPE(sample::Derived)名前空間の外に書く。セミコロン不要。
  • CEREAL_REGISTER_POLYMORPHIC_RELATION(sample::Base, sample::Derived)名前空間の外に書く。セミコロン不要。
  • Derivedはデフォルトコンストラクタを持っていないといけないっぽい
#include <cereal/types/base_class.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/memory.hpp>
#include <cereal/archives/binary.hpp>
#include <fstream>
#include <string>
#include <memory>

namespace sample
{

class Base
{
  public:
    virtual std::string name() const = 0;
    virtual std::string parameter() const = 0;

    template<typename Archive>
    void serialize(Archive&)
    {
        return;
    }
};

class Derived : public Base
{
  public:
    Derived()  = default;
    ~Derived() = default;

    Derived(std::string p) : parameter_(std::move(p)){}

    std::string name()      const override {return "Derived";}
    std::string parameter() const override {return this->parameter_;}

    template<typename Archive>
    void serialize(Archive& ar)
    {
        ar(cereal::base_class<Base>(this), parameter_);
        return;
    }

  private:
    std::string parameter_;
};


template<typename T, typename ... Ts>
std::unique_ptr<T> make_unique(Ts&& ... args)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(args)...));
}
} // sample

CEREAL_REGISTER_TYPE(sample::Derived)
CEREAL_REGISTER_POLYMORPHIC_RELATION(sample::Base, sample::Derived)

int main()
{
    {
        std::unique_ptr<sample::Base> der = sample::make_unique<sample::Derived>("hoge");

        std::ofstream os("out.cereal", std::ios::binary);
        cereal::BinaryOutputArchive archive(os);

        archive(der);
        std::cout << "name: " << der->name()      << std::endl;
        std::cout << "para: " << der->parameter() << std::endl;
    }
    std::cout << "=====================================================\n";
    {
        std::unique_ptr<sample::Base> bs;

        std::ifstream is("out.cereal", std::ios::binary);
        cereal::BinaryInputArchive archive(is);
        archive(bs);

        std::cout << "name: " << bs->name()      << std::endl;
        std::cout << "para: " << bs->parameter() << std::endl;
    }
    return 0;
}

以下ではソースを読みもしていないのにcerealが中で何をしているか想像している。


自分でシリアライズライブラリを書こうとしたときの経験からして、恐らく上記のマクロで型情報を読んだあと実施にその型を生成するための関数なんかを用意しているのだろう。別個に書いても動くこと、ヘッダオンリーであることから、恐らくstaticなコンテナがcerealの奥深くにあって、このマクロでそこにBaseからDerivedにキャストする関数か関数オブジェクトを登録していくのだろう。動的な型情報、型名など、で分岐してその型のインスタンスを作らないといけないので、テンプレート関数かテンプレート構造体を特殊化して入れているのではないか? ある型のインスタンスを作る関数は静的に作っておかないといけないわけだし、その目的にはテンプレート特殊化が一番手っ取り早かろう。

// 普通に自前で実装するなら以下のような感じになると思われる。
std::unique_ptr<Base> load(const archive& data)
{
    if(data.name == "Derived")
    {
        // ここに、名前と型の関係がハードコードされていると解釈できる
        return make_unique<Derived>(load<Derived>(data));
    }
    if(data.name == "Derived2")
    {
        // これをあり得る全パターンやるか、マップを作って管理しておく
    }
}

// とはいえライブラリはユーザー定義クラスを事前に知ることはできない。
// なので上のような愚直なコードは書けない。
// なら、上の if ブロック一つ分に相当する関数をそれぞれ作成して登録し、
// 名前からテーブル引きするしかないだろう。
Base* load(archive data)
{
    // 上でハードコードされていた関係をここでマップとして持っている
    const auto loader = loaders<Base>::get_loaders(data.name);
    return loader(data);
}

この想像があっていた場合、そういう関数オブジェクトは普通に考えてcerealの内部実装に近いところだから、cereal::detailとかに定義されていると思われる。cereal::implとかかもだが。なのでマクロはnamespaceをその場で開くはずなので、ユーザー定義名前空間の中でCEREAL_REGISTER_TYPEを書いても動かない。

まあこんな単純じゃないだろうが。想像で説明するくらいならソースコードを読め。