はじめに
わけあってstd::mapをシリアライズ/デシリアライズしたい。boost::serializationを使えば一発らしいんだけど、わけあってあまりboostを使いたくない。
というわけで車輪の再開発をする。書いたコードは
においておく。
方針
std::mapを継承したserializable_mapクラスを作る。こういう感じ。
template<class Key, class Value>
class serializable_map : public std::map<Key, Value> {
// ここを実装したい
};
シリアライズとデシリアライズメンバ関数は、こんな形になっていてほしい。
std::vector<char> serialize() {
std::vector<char> buffer;
std::stringstream ss;
for (auto &i : (*this)) {
Key str = i.first;
Value value = i.second;
write(ss, str);
write(ss, value);
}
size_t size = ss.str().size();
buffer.resize(size);
ss.read(buffer.data(), size);
return buffer;
}
void deserialize(std::vector<char> &buffer) {
offset = 0;
while (offset < buffer.size()) {
Key key;
Value value;
read(buffer, key);
read(buffer, value);
(*this)[key] = value;
}
}
つまり、シリアライズは全てのキーと値をstd::stringstreamに突っ込んで、最後にstd::vector<char>に変換してそれを返す。デシリアライズはstd::vector<char>から直接読み込む。書き込みはstd::stringstreamを使ってるので書き込み位置の制御は不要だが、読み込みはstd::vector<char>を使ってるので、現在の読み込み位置を覚えておく必要がある。それをoffsetで覚えている1。
基本的にテンプレートを使うが、std::stringだけ別扱いにする。あとはこのwriteとreadを実装すれば良い。
実装
読み込み、書き込みともに、intとかそういうPODなら
template<class T>
void write(std::stringstream &ss, T &t) {
ss.write((char*)(&t), sizeof(t));
}
template<class T>
void read(std::vector<char> &buffer, T &t) {
t = (T)(*(buffer.data() + offset));
offset += sizeof(T);
}
でいける。
ただし、std::stringの場合は長さが不定なので、最初に長さを、次に実体を書き込む必要がある。というわけで、そういうふうにテンプレートを特殊化する。
void write(std::stringstream &ss, std::string &str) {
size_t size = str.size();
ss.write((char*)(&size), sizeof(size));
ss.write((char*)(str.data()), str.length());
}
void read(std::vector<char> &buffer, std::string &str) {
size_t size = (int)(*(buffer.data() + offset));
offset += sizeof(size_t);
std::string str2(buffer.data() + offset, buffer.data() + offset + size);
str = str2;
offset += size;
}
後はテストのために、中身を表示する関数showも作っておこう。
void show(void) {
for (auto &i : (*this)) {
std::cout << i.first << ":" << i.second << std::endl;
}
std::cout << std::endl;
}
これで完成。
テスト
こんなテストを書いた。
void
test1(void) {
std::cout << "string->int" << std::endl;
serializable_map<std::string, int> m;
m["test1"] = 1;
m["test2"] = 2;
m["test3"] = 3;
std::vector<char> buffer = m.serialize();
serializable_map<std::string, int> m2;
m2.deserialize(buffer);
m2.show();
}
void
test2(void) {
std::cout << "int->string" << std::endl;
serializable_map<int, std::string> m;
m[1] = "test1";
m[2] = "test2";
m[3] = "test3";
std::vector<char> buffer = m.serialize();
serializable_map<int, std::string> m2;
m2.deserialize(buffer);
m2.show();
}
void
test3(void) {
std::cout << "int->int" << std::endl;
serializable_map<int, int> m;
m[1] = 1;
m[2] = 2;
m[3] = 3;
std::vector<char> buffer = m.serialize();
serializable_map<int, int> m2;
m2.deserialize(buffer);
m2.show();
}
int
main(void) {
test1();
test2();
test3();
}
つまり、<std::string, int>、<int, std::string>, <int, int>のそれぞれについてシリアライズ、デシリアライズを試している。実行結果はこうなる。
$ g++ -std=c++11 test.cpp -o a.out
$ ./a.out
string->int
test1:1
test2:2
test3:3
int->string
1:test1
2:test2
3:test3
int->int
1:1
2:2
3:3
うまくいってるみたいですね。
まとめ
std::mapをboostを使わずにシリアライズしたかったので車輪の再開発をした。読み込みのところで、最初は返り値の型によるオーバーロードをやろうとしたんだけど、ナイーブにやるとクラス内の関数テンプレートの特殊化が必要になって面倒くさい(たぶん)。いろいろ考えたんだけど、結局引数で特殊化するのが一番簡単な気がしてそうしてしまった。
どうでもいいけど、テンプレートの実装をミスるとやたら大量のエラーメッセージ出るし意味不明だしで困ったもんですね。
-
なんで読み込みを
std::istream系で実装しなかったのか覚えてない。なんか理由があった気がする。 ↩