株式会社エス・スリー・フォー

時空を越えるオブジェクト

時空を越えるオブジェクト

シリアライズってなに?

シリアライズ(あるいはストリーミング)とは、オブジェクトの状態をバイト列に変換すること、そしてバイト列から元のオブジェクトを復元することです。バイト列に変換できればそれをファイルに保存することで、一旦終了したアプリケーションが再度起動したときに前回の状態に戻すことができますし、ネットワーク上で共有すれば複数のアプリケーションが同じオブジェクトを利用できます。また、変換されたバイト列をソケットやRS232Cに流し込めば遠く離れたマシン上で復元することもできるでしょう。簡単に言えばオブジェクトをファイルに書き込むこと、そしてファイルから読み込むことです。ソケットやRS232Cもバイト列の転送媒体と言う観点からは広義のファイルと考えていいでしょうからね。

Step-0:簡単なシリアライズ

シリアライズなんて御大層な用語ですけど、要するにファイルに対するsaveloadです。そう難しくかんがえることはありません。

class Foo {
  int n_;
public:
  explicit Foo(int n =0) : n_(n) {}
  void set(int n) { n_ = n; }
  void printOn(std::ostream& strm) cons {
    strm << "Foo:" << n_;
  }
};

なんてなクラスにsave/loadメソッドを追加して、シリアライズを実現しましょう。

class Foo {
  int n_;
public:
  explicit Foo(int n =0) : n_(n) {}
  void set(int n) { n_ = n; }
  void printOn(std::ostream& strm) const
     { strm << "Foo:" << n_ };
  void save(FILE*) const;
  void load(FILE*);
};

void Foo::save(FILE* fp) const {
  fwrite(&n_,sizeof(n_),1,fp);
}

void Foo::load(FILE* fp) {
  fread(&n_,sizeof(n_),1,fp);
}

/* お試し */

int main() {
  FILE* fp;

  // save
  Foo x(5);
  x.printOn(cout);
  fp = fopen("foo.dat","wb");
  x.save(fp);
  fclose(fp);

  // load
  Foo y;
  fp = fopen("foo.dat","rb");
  y.load(fp);
  fclose(fp);
  y.printOn(cout);

  return 0;
}

どうということはありませんな。

Step-1:メディアの抽象化

上の例ではファイルストリーム(FILE*)に対するsave/loadを実装しました。同様にfstreamやソケット、RS232Cなどなどに対するsave/loadを追加すれば様々なメディアにシリアライズできますが、各メディア毎にメソッドが2つづつ追加されるのもカッコよくありません。メディアの違いを吸収するクラスSaverLoaderを作りましょう。

namespace ser {

  class Loader {
  public:
    virtual void read(void*,size_t) =0;
            void read(bool& v)           { read(&v,sizeof(v)); }
            void read(char& v)           { read(&v,sizeof(v)); }
            void read(unsigned char& v)  { read(&v,sizeof(v)); }
            void read(short& v)          { read(&v,sizeof(v)); }
            void read(unsigned short& v) { read(&v,sizeof(v)); }
            void read(int& v)            { read(&v,sizeof(v)); }
            void read(unsigned int& v)   { read(&v,sizeof(v)); }
            void read(long& v)           { read(&v,sizeof(v)); }
            void read(unsigned long& v)  { read(&v,sizeof(v)); }
            void read(float& v)          { read(&v,sizeof(v)); }
            void read(double& v)         { read(&v,sizeof(v)); }
   };

  class Saver {
  public:
    virtual void write(const void*,size_t) =0;
            void write(bool v)           { write(&v,sizeof(v)); }
            void write(char v)           { write(&v,sizeof(v)); }
            void write(unsigned char v)  { write(&v,sizeof(v)); }
            void write(short v)          { write(&v,sizeof(v)); }
            void write(unsigned short v) { write(&v,sizeof(v)); }
            void write(int v)            { write(&v,sizeof(v)); }
            void write(unsigned int v)   { write(&v,sizeof(v)); }
            void write(long v)           { write(&v,sizeof(v)); }
            void write(unsigned long v)  { write(&v,sizeof(v)); }
            void write(float v)          { write(&v,sizeof(v)); }
            void write(double v)         { write(&v,sizeof(v)); }
   };
}

FILE*に対するシリアライズにはSaver/Loaderから導出したFileSaver/FileLoaderを作り、純粋仮想関数write(const void*,size_t)/read(void*,size_t)を再定義します。

namespace ser {

  class FileLoader : public Loader {
    FILE* fp_;
  public:
    explicit FileLoader(FILE* fp) : fp_(fp) {}
    virtual void read(void*,size_t);
  };

  void FileLoader::read(void* v, size_t n) {
    fread(v,n,1,fp_);
  }

  class FileSaver : public Saver {
    FILE* fp_;
  public:
    explicit FileSaver(FILE* fp) : fp_(fp) {}
    virtual void write(const void*,size_t);
  };

  void FileSaver::write(const void* v, size_t n) {
    fwrite(v,n,1,fp_);
  }

}

iostreamに対しても同様に、

namespace ser {

  class StreamLoader : public Loader {
    std::istream& s_;
  public:
    explicit StreamLoader(std::istream& s) : s_(s) {}
    virtual void read(void*,size_t);
  };

  void StreamLoader::read(void* v, size_t n) {
    s_.read(static_cast<char*>(v), n);
  }

  class StreamSaver : public Saver {
    std::ostream& s_;
  public:
    explicit StreamSaver(std::ostream& s) : s_(s) {}
    virtual void write(const void*,size_t);
  };

  void StreamSaver::write(const void* v, size_t n) {
    s_.write(static_cast<const char*>(v), n);
  }

}

これに伴い、Fooを書き換えます。

class Foo {
  int n_;
public:
  explicit Foo(int n =0) : n_(n) {}
  void set(int n) { n_ = n; }
  void printOn(std::ostream& strm) const
    { strm << "Foo:" << n_; }
  void save(ser::Saver&) const;
  void load(ser::Loader&);
};

void Foo::save(ser::Saver& s) const {
  s.write(n_);
}

void Foo::load(ser::Loader& l) {
  l.read(n_);
}

/* お試し */

int main() {
  FILE* fp;

  // save
  Foo x(5);
  x.printOn(cout);
  fp = fopen("foo.dat","wb");
  ser::FileSaver s(fp);
  x.save(s);
  fclose(fp);

  // load
  Foo y;
  fp = fopen("foo.dat","rb");
  ser::FileLoader l(fp);
  y.load(l);
  fclose(fp);
  y.printOn(cout);

  return 0;
}

さて、これでメンバ変数がchar,intなどの単純な型で構成されているクラスであれば正しくシリアライズできるでしょうよ。

では、メンバ変数にポインタを含む場合はどうでしょう。

class Link {
  int   n_;
  Link* next_;
public:
  explicit Link(int n =0, Link* next =0) : n_(n), next_(next) {}
  ‾Link() { delete next_; }
  void save(ser::Saver&) const;
  void load(ser::Loader&);
};

このオブジェクトをsave/loadするにはどうすればいいでしょうか。

void Link::save(ser::Saver& s) const {
  s.write(n_);
  next_->save(s);
}

void Link::load(ser::Loader& l) {
  l.read(n_);
  delete next_;
  next_ = new Link;
  next_->load(l);
}

…残念でした。このコードではおそらくまともには動かないでしょうよ。next_が0であるときの考慮がなされてませんからね。正しくは、ポインタが0であるか否かのフラグを書き込んでおき、load時にチェックしないとね。

void Link::save(ser::Saver& s) const {
  s.write(n_);
  bool flag = (next_ != 0);
  s.write(flag);
  if ( flag ) next_->save(s);
}

void Link::load(ser::Loader& l) {
  l.read(n_);
  delete next_;
  next_ = 0;
  bool flag;
  l.read(flag);
  if ( flag ) {
    next_ = new Link;
    next_->load(l);
  }
}

Step-2:Polymorphicなシリアライズ

ポインタをシリアライズするときの問題はまだあります。たとえばさきほどのLinkからLink2を導出します。

class Link2 : public Link {
public:
  explicit Link2(int n =0, Link* next =0) : Link(n,next) {}
  ...
};

Linkのメンバ変数Link* next_にはLinkおよびその派生クラスのポインタが代入できますから、

Link lnk(1,new Link2(2));

何てことやっても構いません。
lnksaveし、loadすると正しく復元されるでしょうか?
ま、ダメでしょうね。Link::loadの中でnewできるのはLinkだけですからね。

これを正しくシリアライズするには、書き込んだオブジェクトが何であるかを表すIDをsave時に打ち込み、load時には読み込んだIDに対応するオブジェクトを生成しなければなりません。

そのために、まずシリアライズ可能なオブジェクトのベースクラスを用意します。

namespace ser {

  class Saver;
  class Loader;

  class Object {
  public:
    virtual ‾Object() {}
    virtual long id() const =0;
    virtual void save(Saver&) const =0;
    virtual void load(Loader&) =0;
  };

}

シリアライズしたいすべてのクラスはこのser::Objectから導出し、仮想関数id()がクラス毎に0でないユニークな値を返すよう再定義します(0はnullポインタを表すために用います)。

class Foo : public ser::Object {
public:
  virtual long id() const { return 1; }
  ...
};

class Bar : public ser::Object {
public:
  virtual long id() const { return 2; }
  ...
};

Saverには新たなメソッドwriteObjectを追加します。

namespace ser {

  class Saver {
  public:
     void writeObject(const Object*);
     ...
  };

  void Saver::writeObject(const Object* obj) {
    long id = obj ? obj->id() : 0;
    write(id);
    if ( id ) obj->save(*this);
  }

}

次に、IDに対応するオブジェクトを生成するためのクラスFactoryをこさえます。

namespace ser {

  class Factory {
    typedef Object* (*create_fun)();
    typedef std::map<long,create_fun> omap;
    typedef std::auto_ptr<Factory> instance_ptr;
    friend instance_ptr;
    static instance_ptr instance_;
    omap map_;
    Factory() {}
    ‾Factory() {}
  public:
    static Factory* instance();
    void regist(long,create_fun); // 登録
    Object* create(long);         // 生成
  };

  Factory* Factory::instance() {
    if ( !instance_.get() )
      instance_ = instance_ptr(new Factory);
    return instance_.get();
  }

  void Factory::regist(long id, create_fun fn) {
    map_[id] = fn;
  }

  Object* Factory::create(long id) {
    omap::iterator it = map_.find(id);
    return ( it == map_.end() ) ? 0 : it->second();
  }

  std::auto_ptr<Factory> Factory::instance_;

}

Loaderに追加するメソッドreadObjectFactoryの助けを借りてオブジェクトの生成と読み込みを行ないます。

namespace ser {

  class Loader {
  public:
     Object* readObject();
     ...
   };

  Object* Loader::readObject() {
    Object* obj = 0;
    long id;
    read(id);
    if ( id ) {
      obj = Factory::instance()->create(id);
      obj->load(*this);
    }
    return obj;
  }
}

アプリケーションはシリアライズに先立ってFactoryへのオブジェクトの登録を行なっておかなければなりません。

ser::Object* foo_() { return new Foo; }
ser::Object* bar_() { return new Bar; }

void init() {
  ser::Factory* factory = ser::Factory::instance();
  factory->regist(1, &foo_);
  factory->regist(2, &bar_);
}

int main() {
  init();
  ...
  return 0;
}

Foo/Barのシリアライズは以下のようなコードで実現できます。

  init(); // 少なくともloadの前にはコールしておくこと!

  // save
  Foo* foo = new Foo(1);
  Bar* bar = new Bar(2);
  ofstream strm("objcts.dat",ios_base::binary);
  ser::StreamSaver s(strm);
  s.writeObject(foo);
  s.writeObject(bar);

  // load
  ifstream strm("objects.dat",ios_base::binary);
  ser::StreamLoader l(strm);
  Foo* foo = static_cast<Foo*>(l.readObject());
  Bar* bar = static_cast<Bar*>(l.readObject());

Step-3:共有オブジェクトのシリアライズ

シリアライズはこれでカンペキ?
いや、もうひとつ、ややこしい問題が残っています。

class Human : public ser::Object {
  char*  name_;   // 名前
  Human* spouse_; // 配偶者
public:
  explicit Human(const char*);
  virtual‾ Human()         { delete[] name_; }
  void marry(Human* h)     { spouse_ = h; }
  const char* name() const { return name_; }
  void printOn(std::ostream&) const;
  virtual long id() const;
  void save(ser::Saver&) const;
  void load(ser::Loader&);
};

Human::Human(const char* n) : spouse_(0) {
  name_ = new char[strlen(n)+1];
  strcpy(name_,n);
}

long Human::id() const {
  return 3;
}

void Human::printOn(std::ostream& strm) const {
  strm << "名前:" << name()
       << " 配偶者:" << ( spouse_ ? spouse_->name() : "なし");
}

void Human::save(ser::Saver& s) const {
  long len = strlen(name())+1;
  s.write(len);
  s.write(name(),len);
  s.writeObject(spouse_);
}

void Human::load(ser::Loader& l) {
  long len;
  l.read(len);
  delete[] name_;
  name_ = new char[len];
  l.read(name_,len);
  delete spouse_;
  spouse_ = static_cast<Human*>(l.readObject());
}

クラスHumanは名前、そして配偶者へのポインタを持っています。

  ofstream strm("serialize.dat",ios_base::binary);
  ser::StreamSaver s(strm);
  Human* he  = new Human("アダム");
  Human* she = new Human("イヴ");
  he->marry(she);
  she->marry(he);
  s.writeObject(he);
  s.writeObject(she);
  delete he;
  delete she;

"アダム"と"イヴ"はめでたく夫婦になりました。市役所の役人は"アダム"と"イヴ"をシリアライズします。

"アダム"が書き込まれるとき、彼の配偶者"イヴ"を書き込みます。
すると"イヴ"の書き込みの中で、彼女の配偶者"アダム"を書き込みます。
はたまた"アダム"の書き込みの中で、彼の配偶者"イヴ"を書き込みます。
…そう、無限ループに陥るのです。

この問題を回避するには、Loader::writeObject/Saver::readObjectにもうひとヒネリしなければなりません。一度書き込まれたオブジェクトは二度と書き込まないからくりが必要です。

Saver::writeObjectでは、オブジェクトを書き込んだときそれが通算何番目に書き込まれたかを示すシリアル番号を記録しておきます。そして二度目以降の書き込みでは記録をたどってシリアル番号だけを書き込みます。

namespace ser {

  class Saver {
    typedef std::map<Object*,long> omap;
    omap map_;
    long cnt_;
  public:
    Saver() : cnt_(0) {}
    void writeObject(const Object*);
    ...
   };

  void Saver::writeObject(const Object* obj) {
    if ( obj ) {
      omap::iterator it = map_.find(const_cast<Object*>(obj));
      if ( it != map_.end() ) {
        write(it->second);
      } else {
        write(-obj->id());
        map_[const_cast<Object*>(obj)] = ++cnt_;
        obj->save(*this);
      }
    } else {
      long id = 0;
      write(id);
    }
  }

}

Loader::readObjectでは、オブジェクトを読み込んだときそれが通算何番目に読み込んだかを記録しておきます。そしてシリアル番号を読み込んだら記録をたどってポインタ値を検索します。

namespace ser {

  class Loader {
    typedef std::vector<Object*> ovec;
    ovec vec_;
    long cnt_;
  public:
    Loader();
    Object* readObject();
    ...
   };

  Loader::Loader() {
    vec_.push_back(0);
  }

  Object* Loader::readObject() {
    Object* obj = 0;
    long id;
    read(id);
    if ( id < 0 ) {
      obj = Factory::instance()->create(-id);
      vec_.push_back(obj);
      obj->load(*this);
    } else
    if ( id > 0 ) {
      obj = vec_[id];
    }
    return obj;
  }

}

ふう…お疲れ様でした。src