#Cereal
C++11以降で利用できるC++シリアライズライブラリ
http://uscilab.github.io/cereal/index.html
何が強い?
・ヘッダのみで構成されているのでリンク作業必要なし
・様々な手法でシリアライズを定義出来る
・クラス内外関係なしにシリアライズの定義を探し出す
・例え継承してようが仮想継承してようがシリアライズ可能
・privateコンストラクタしかなくても、friend class cereal::access;で解決する
・名前だけで検索するから変なclassを継承しなくてもOK virtual化も要らない
・その気になれば独自templateストレージクラスもシリアライズ可能
・STLコンテナに標準で対応しており、includeするだけでコンテナをシリアライズ可能
・スマートポインタもシリアライズ可能
・XML / JSON / Binary の3形式に対応
・英語だが、公式の情報が割と充実している 一度は読んでおくべし
##使い方
cerealをダウンロードする
http://uscilab.github.io/cereal/index.html (2017/04/13 現在、最新はver1.22)
解凍し、include/cerealをプロジェクトの追加のincludeパスに設定する
必要なinclude
-
cereal.hppをinclude
-
archivesフォルダにある
- binary
- portable_binary
- json
- xml
からシリアライズしたい形式のヘッダをinclude
-
typesフォルダからシリアライズしたいSTLの型に対応するヘッダをinclude
- string → std::string
- vector → std::vector
- list → std::list
- unordered_map → std::unordered_map
他多数 普通に使う分にはまあ困らないはず
シリアライズするための関数を定義
実際にデータをシリアライズする関数は次節で解説
いくつか手法があるのでサクッと試す
class Vector3{
public:
float x,y,z;
template<class T>
void serialize(T &archive) {
archive(
CEREAL_NVP(x),
CEREAL_NVP(y),
CEREAL_NVP(z)
);
}
}
一番簡単なシリアライズ関数
cerealはテンプレートメタクソプログラミングによって、関数の名前だけでシリアライズする関数を特定しているらしい
名前が気に入らないならmacros.hppにあるCEREAL_SERIALIZE_FUNCTION_NAMEを書き換えればOK
archive()の引数では、「出力時、xやyにどのような名前を付けるか」を指定できる
CEREAL_NVPマクロは、変数名をそのまま出力名にするため、上記Vector3.hの出力は以下のようになる
{
"x":1.0,
"y":1.0,
"z":1.0
}
とりあえず名前を変えてみる
class Vector3{
public:
float x,y,z;
template<class T>
void serialize(T &archive) {
archive(
cereal::make_nvp("value1", x),
cereal::make_nvp("value2", y),
cereal::make_nvp("value3", z)
);
}
}
{
"value1":1.0,
"value2":1.0,
"value3":1.0
}
改悪じゃねえか
シリアライズする
今回はJsonで入出力してみる
void Output(const Vector3 &v, const std::string &path){
//出力時の受け皿
std::stringstream stream;
//出力用の型に文字列streamを登録
cereal::JSONOutputArchive jsonOutArchive(stream);
//文字列ストリームにjsonを投げつけてもらう
jsonOutArchive(cereal::make_nvp("TestTag", v));
//ファイル出力用ストリーム作成
std::ofstream outputFile(path, std::ios::out);
//書き出す
outputFile << stream.str();
//閉じる
outputFile.close();
stream.clear();
}
void Input(std::Vector3 &v, const std::string &path){
//入力される文字列受け皿
std::stringstream stream;
//ファイル入力ストリーム作成
std::ifstream inputFile(path, std::ios::in);
//入力データを全部文字列streamに投げる
stream << inputFile.rdbuf();
//jsonをロード
cereal::JSONInputArchive jsonInputArchive(stream);
//デコードしたデータをvにセット
jsonInputArchive(cereal::make_nvp("TestTag", v));
}
ポインタのシリアライズ
cerealでは 生のポインタをシリアライズする事は出来ない が、
スマートポインタのシリアライズ に対応している
よって、以下のようなシリアライズが通る
void Output(std::shared_ptr<Vector3> v, const std::string &path){
//...
jsonOutArchive(cereal::make_nvp("TestTag", v));
//...
}
Jsonでの出力結果
{
"TestTag" : {
"ptr_wrapper": {
"id": 2147483649,
"data": {
"x":1.0,
"y":1.0,
"z":1.0
}
}
}
}
このようにポインタにIDが振られるようになる
同一インスタンスを管理するshared_ptrを複数同時にシリアライズさせた場合、一つだけにdata{}を持たせるようになっている
Polymorphicな型のシリアライズ
こんな感じのソースをシリアライズしたい
class Object{
public:
virtual void Update() = 0;
virtual ~Object() = default;
}
class Character : public Object{
public:
virtual void Update()override {
//...
}
}
Polymophic = 多態性 つまりはvirtualなclassかどうかだと思えばいい
内部ではstd::is_polymorphic<>によるチェックが行われており、これに合致した物は
スマートポインタのシリアライズ時の処理が若干変わる
virtualを利用する処理を書く という事は、恐らく以下のような実装がされているはずである
class Object{
public:
virtual void Update() = 0;
static void AllUpdate(){
for(auto &n : objects){
n->Update();
}
}
private:
static std::vector<std::shared_ptr<Object>> objects;
}
class Character : public Object{
public:
virtual void Update()override {
//...
}
}
要点を言うと派生型を基底型にキャストし、基底型からvirtual関数を呼び出している 訳である
シリアライズ時でもこれと同じことが出来るようにする
CEREAL_REGISTER_TYPE(T)
マクロを利用し、コンパイル時にクラスの「登録」を行う(詳しくはマクロの定義に飛ぶとおぞましい量のコメントがある
CEREAL_REGISTER_TYPE_WITH_NAME(T,"")
を使用すると型の名前も保存できる
これにより、基底クラスのポインタをシリアライズした際であっても、派生クラスの情報が保存される
もちろん、デシリアライズ時でも派生クラスの情報を取った後基底クラスにキャストされる メチャクチャ便利
Jsonで出力
{
"polymorphic_id": 2147483649,
"polymorphic_name": "Character", //CEREAL_REGISTER_TYPE_WITH_NAMEの""部分
"ptr_wrapper": {
"id": 2147483651,
"data": {
...
}
}
こんな感じでpolymorphicな情報も載るようになる
困ったこと
Q.Json / XMLでstd::wstring型が使えないんだけど
A.使えない(多分)https://github.com/USCiLab/cereal/issues/95
Q.シリアライズ前処理、後処理とかって書ける?
A.prologue / epilogue関数を定義すると呼んでくれる
それとserialize以外にもsave / load みたいな関数もあるらしい
SaveLoad
・保存時はパスのみ、読み込み時はパスを利用して外部の関数を呼び出す なんて事が出来る
template<class T>
void save(T &archive) const{
archive(
CEREAL_NVP(x),
CEREAL_NVP(y),
CEREAL_NVP(z)
);
}
template<class T>
void load(T &archive) {
archive(
CEREAL_NVP(x),
CEREAL_NVP(y),
CEREAL_NVP(z)
);
}
Q.エラー出力が自分のソースコードより多くて読む気失せる
A.templateが使われている全てのエラーに言える事だが、「エラーの根本」を探す
経由地点の情報はすべて無視するぐらいでOK
以下個人的に引っかかったもの
パターン1
template<class T>
void save(T& archive) {
archive(
cereal::make_nvp("Position", _localPosition),
cereal::make_nvp("Rotation", _localRotation) //,が無い
cereal::make_nvp("Scale", _localScale),
cereal::make_nvp("Parent", _parent), //末尾は,不要
);
}
パターン2
template<class T>
void save(T& archive) {
std::string _path = R"(C:\Users\UserName\Desktop)";//単一のバックスラッシュはRapidJsonが対応していない?
archive(
cereal::make_nvp("Position", _localPosition),
cereal::make_nvp("Rotation", _localRotation),
cereal::make_nvp("Scale", _localScale),
cereal::make_nvp("Parent", _parent),
cereal::make_nvp("Path", _path)
);
}
パターン3
class Transform2D : public Transform{
...
template<class T>
void serialize(T& archive) {
archive(
cereal::base_class<Transform>(this), //上位クラスのシリアライズ関数を実行出来る
cereal::make_nvp("Position", _localPosition),
cereal::make_nvp("Rotation", _localRotation),
cereal::make_nvp("Scale", _localScale),
cereal::make_nvp("Parent", _parent)
);
}
}
//CEREAL_REGISTER_TYPE(Transform) //登録するのは派生先クラス
//CEREAL_REGISTER_ARCHIVE(Transform2D) //補完に頼ると書きがち
CEREAL_REGISTER_TYPE(Transform2D)//正解
エラー1
一致するオーバーロードされた関数が見つかりませんでした。 (ソース ファイルをコンパイルしています ファイル.cpp)
-> シリアライズ対象になっているメンバの型サイズがヘッダの段階で予測できていない
-> serializeをcppに落とし込むか定義を完結させる
-> serialize()にconstが付いている
Q.もうちょっとしっかり知りたい
A.http://uscilab.github.io/cereal/assets/doxygen/index.html を漁ったり、https://github.com/USCiLab/cereal/issues を見ると割と幸せになるかも
まとめ
・手間こそあるものの、機能性 / 汎用性共にメチャクチャ強い
・Binaryで出せば速度も申し分無いので実用的
・polymophicな継承関係をシリアライズする時の挙動を如何に早く理解できるかがカギ
・出来る事が多すぎて全てまとめきれない 時々追記するかも