std::mapのシリアライズ

はじめに

わけあってstd::mapをシリアライズ/デシリアライズしたい。boost::serializationを使えば一発らしいんだけど、わけあってあまりboostを使いたくない。

というわけで車輪の再開発をする。書いたコードは

https://gist.github.com/kaityo256/eb6a49cb40f99b97898f5e464c2c208f

においておく。

方針

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だけ別扱いにする。あとはこのwritereadを実装すれば良い。

実装

読み込み、書き込みともに、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を使わずにシリアライズしたかったので車輪の再開発をした。読み込みのところで、最初は返り値の型によるオーバーロードをやろうとしたんだけど、ナイーブにやるとクラス内の関数テンプレートの特殊化が必要になって面倒くさい(たぶん)。いろいろ考えたんだけど、結局引数で特殊化するのが一番簡単な気がしてそうしてしまった。

どうでもいいけど、テンプレートの実装をミスるとやたら大量のエラーメッセージ出るし意味不明だしで困ったもんですね。

  1. なんで読み込みをstd::istream系で実装しなかったのか覚えてない。なんか理由があった気がする。