時空を越えるオブジェクト
時空を越えるオブジェクト
シリアライズってなに?
シリアライズ(あるいはストリーミング)とは、オブジェクトの状態をバイト列に変換すること、そしてバイト列から元のオブジェクトを復元することです。バイト列に変換できればそれをファイルに保存することで、一旦終了したアプリケーションが再度起動したときに前回の状態に戻すことができますし、ネットワーク上で共有すれば複数のアプリケーションが同じオブジェクトを利用できます。また、変換されたバイト列をソケットやRS232Cに流し込めば遠く離れたマシン上で復元することもできるでしょう。簡単に言えばオブジェクトをファイルに書き込むこと、そしてファイルから読み込むことです。ソケットやRS232Cもバイト列の転送媒体と言う観点からは広義のファイルと考えていいでしょうからね。
Step-0:簡単なシリアライズ
シリアライズなんて御大層な用語ですけど、要するにファイルに対する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) 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つづつ追加されるのもカッコよくありません。メディアの違いを吸収するクラスSaver
とLoader
を作りましょう。
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));
何てことやっても構いません。
lnk
をsave
し、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
に追加するメソッドreadObject
はFactory
の助けを借りてオブジェクトの生成と読み込みを行ないます。
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; } }