boost::serializationを使う

はじめに

boost C++ Libraries(以下単にboost)という非常に便利なライブラリがあります。 次期C++標準にこのライブラリのうちのいくつかが採用されるらしいという話もあり、 C++開発者にとっては無視して通れないライブラリです。

そんなboostの中に、serializationというライブラリがあります。 一言で言ってしまえば、「クラスの状態をファイルに書き出したり、復元するためのライブラリ」といったところでしょうか。 MFCのCArchiveクラスを使ったことのある方にはおなじみだと思います。 04/07/21現在、まだ正式版には含まれてはいませんが、いずれ含まれるでしょうということで、 ちょいと先走って使い方の解説をします。 願わくば、この記事を書いた後に大幅な変更などがありませんよう……

あと、このserializationって名前長くてタイプするの面倒なんですが。 なんかもちっと短い名前なかったのかなーと思ってしまう次第です。

コンパイル

このライブラリは、regexやfilesystemと同じように、ビルドしないと使用できません。

具体的なビルドの仕方は、こちらをご覧ください。

boost::serializationを使用したプログラムのコンパイルの際には、 明示的にライブラリファイルをリンクしてやる必要があるようです。 ここの命令規則とにらめっこしながら、 適切なライブラリファイルをリンクしてください。

基本的に必要なのは、(lib)boost_serialization_*.libで、 ワイド文字列のアーカイブを使う場合にはそれに加えて(lib)boost_wserialization_*.libが必要となるようです。

簡単な使い方

まずは簡単な例題として、下のようなStudentクラスをシリアル化する場合のことを考えてみます。 例題のため、極限まで簡略化してあります。

001 
002 #include <string>
003 
004 class Student {
005 private:
006   std::string name_;
007   int age_;
008 };

このクラスに、シリアル化できるようにコードを書き加えたものが以下のコードです。

001 
002 #include <string>
003 #include <boost/serialization/string.hpp>
004 
005 class Student {
006 private:
007   std::string name_;
008   int age_;
009   
010   friend class boost::serialization::access;
011   template<class Archive>
012     void serialize(Archive& ar, const unsigned int version)
013     {
014       ar & name_;
015       ar & age_;
016     }
017 };

それぞれの追加行について説明していきます。

003 #include <boost/serialization/string.hpp>

std::stringクラスをシリアル化するために必要なヘッダーです。 同様に、例えばstd::listクラスをシリアル化するのならばboost/serialization/list.hppが必要になります。 このように特別のインクルードが必要なクラスを以下に挙げます。

それぞれ必要なヘッダー名は、boost/serialization/<クラス名>.hppです。 私としてはC++標準にもかかわらず無視されているvalarrayとbitsetが可哀想で可哀想で……。

010   friend class boost::serialization::access;

boost::serializationライブラリが、privateメンバであるserialize関数にアクセスするためのfriend指定です。 おまじないみたいなものだと考えてください。 なお、このおまじないが面倒だからといって、serialize関数をpublicにしてはいけません。理由は後述します。

011   template<class Archive>
012     void serialize(Archive& ar, const unsigned int version)
013     {
016     }

arは、出力先・入力元を表す引数です。 出力先・入力元の形式には、テキスト形式や、バイナリ形式、XML形式などの種類がありますが、 その種類の違いをテンプレート引数Archiveで吸収しています。

versionは、保存形式のバージョンを示す数値です。 アプリケーションを開発していくにつれて、保存・読込をするデータも変わってくることに備えて、 保存形式のバージョンをチェックしておく必要があるかもしれません。

014       ar & name_;
015       ar & age_;

それぞれのメンバをarに出力・読込します。 C++のストリームなどでは、出力に<<演算子、読込に>>演算子を使いますが、 boost::serializationでは両方あわせて&演算子を使います (<<演算子や>>演算子も使えます。が、この関数は出力と入力両方を受け持つので&演算子を使います)。

実際にデータを入出力するときは、以下のようにします。ここでは、テキスト形式での入出力を説明します。

001 #include <fstream>
002 #include <boost/archive/text_oarchive.hpp>
003 #include <boost/archive/text_iarchive.hpp>
004 
005 void hoge(void) {
006   Student student;
007   
008   // 出力アーカイブの作成
009   std::ofstream ofs("output.txt");
010   boost::archive::text_oarchive oa(ofs);
011   
012   // ファイルに書き出し
013   oa << student;
014   
015   // 出力を閉じる
016   ofs.close();
017   
018   // 入力アーカイブの作成
019   std::ifstream ifs("output.txt");
020   boost::archive::text_iarchive ia(ifs);
021   
022   // ファイルから読込
023   ia >> student;
024   
025   // 入力を閉じる
026   ifs.close();
027 }

大まかな流れとしては、出(入)力ストリームを開いて、それをアーカイブクラスのコンストラクタに渡してアーカイブを作り、 そのアーカイブにデータを出(入)力する、という流れになります。

アーカイブクラスの後始末は不要なようです。ストリームも、ほっとけばいずれ閉じますので、 わざわざcloseしなくても大丈夫そうです。例では直後にifstreamを使う関係上閉じてますが。

なお、入出力形式にはテキスト形式、バイナリ形式、XML形式の3種類があります。 それぞれ対応するアーカイブクラス名を列挙します。

また、テキストアーカイブとXMLアーカイブには、それぞれワイド文字版が用意されています。 ワイド文字版は、コンストラクタにwstreamをとります。

xml系のアーカイブを使用する場合には、後述するNVPを使用する必要があります。 また、ワイド文字列版でないxmlアーカイブは、 出力時に文字コードがUTF-8でなく、OSネイティブな文字コードで保存されます。 にもかかわらず、xml中のエンコーディング指定でUTF-8が指定されていますので、 正しいxmlファイルではないということに留意してください。

クラスにバージョン付けをする

先程のStudentクラスに、新たにメンバ変数を追加した場合のことを考えてみます。 ここでは、生徒の身長を表すheight_メンバ変数を追加してみましょう。

001 
002 #include <string>
003 
004 class Student {
005 private:
006   std::string name_;
007   int age_;
008   int height_;
009 };

このクラスを、単純にserialization対応にすると以下のようになります。

001 
002 #include <string>
003 #include <boost/serialization/string.hpp>
004 
005 class Student {
006 private:
007   std::string name_;
008   int age_;
009   int height_;
010 
011   friend class boost::serialization::access;  
012   template<class Archive>
013     void serialize(Archive& ar, const unsigned int version)
014     {
015       ar & name_ & age_ & height;
016     }
017 };

前回とほとんど同じですので、細かい説明は省きます。 前回説明はしませんでしたが、このように1行に続けて&演算子を使用することも可能です。

この拡張されたStudentクラスを仮にVer.1、前回使用したStudentクラスをVer.0とおきます。 ここで問題としたいのは、Ver.1のStudentクラスで、Ver.0で保存したデータを読み込みたい場合はどうするのか、ということです。

こんな状況のために、引数versionがあるのでしたね。引数versionの値に応じて動作を変えるように書き換えてみましょう。

001 
002 #include <string>
003 #include <boost/serialization/string.hpp>
004 #include <boost/serialization/version.hpp>
005 
006 class Student {
007 private:
008   std::string name_;
009   int age_;
010   int height_;
011 
012   friend class boost::serialization::access;
013   template<class Archive>
014     void serialize(Archive& ar, const unsigned int version) 
015     {
016       if(version > 0) {
017         ar & name_ & age_ & height_;
018       }
019       else {
020         ar & name_ & age_;
021       }
022     }
023 };
024 
025 BOOST_CLASS_VERSION(Student, 1);

変化した部分を、詳しく見ていきましょう。

004 #include <boost/serialization/version.hpp>
025 BOOST_CLASS_VERSION(Student, 1);

クラスにバージョン付けをするには、BOOST_CLASS_VERSIONマクロを使用します。 ここでは、クラスStudentに1というバージョンをつけています。 これによって、関数serializationに渡されるversion引数が変化します。 クラスの内容が変化するたびに、このバージョン番号を1ずつ増やしていけばいいのですね。

基本的に、serialization()関数に渡されるversion引数は、 書き込み時にはBOOST_CLASS_VERSIONマクロで指定された最新のバージョン、 読み込み時には読み込んでいるアーカイブのバージョン(当然現バージョンよりも古いかもしれません)が渡されます。

boost::serializationライブラリでは、「クラスごとに」バージョン付けをします。 そのため、ファイルの保存形式そのものにはバージョン付けをする必要はありません。

016       if(version > 0) {
017         ar & name_ & age_ & height_;
018       }
019       else {
020         ar & name_ & age_;
021       }

ここで、バージョンによって処理を分岐させています。 具体的には、バージョン1より前ならname_とage_を読み書きし、 バージョン1以降ならそれに加えてheight_も読み書きします。

読み込みと書き込みで別の動作を行う

さて、前回の例ですが、よくよく考えてみると、書き込み時にはバージョンのことを考える必要はなさそうです。 だって、書き込むときは常に最新のバージョンなんですから。

そこのところを考えて、書き換えたコードが以下のものになります。

001 
002 #include <string>
003 #include <boost/serialization/string.hpp>
004 #include <boost/serialization/version.hpp>
005 #include <boost/serialization/split_member.hpp>
006 
007 class Student {
008 private:
009   std::string name_;
010   int age_;
011   int height_;
012 
013   friend class boost::serialization::access;
014   BOOST_SERIALIZATION_SPLIT_MEMBER();
015   
016   template<class Archive>
017     void save(Archive& ar, const unsigned int version) const 
018     {
019       ar & name_ & age_ & height;
020     }
021   
022   template<class Archive>
023     void load(Archive& ar, const unsigned int version) 
024     {
025       if(version > 0) {
026         ar & name_ & age_ & height_;
027       }
028       else {
029         ar & name_ & age_;
030       }
031     }
032 };
033 
034 BOOST_CLASS_VERSION(Student, 1);

boost::serializationでは、このように書き込み時と読み込み時で別々の関数に分岐させることもできます。 以下に各追加行の説明をします。

005 #include <boost/serialization/split_member.hpp>
014   BOOST_SERIALIZATION_SPLIT_MEMBER();

読み書きを別々の関数に分離するには、BOOST_SERIALIZATION_SPLIT_MEMBERマクロを使用します。 このマクロを使用することで、serialization関数をload関数とsave関数に分割することができます。

016   template<class Archive>
017     void save(Archive& ar, const unsigned int version) const 
018     {
019       ar & name_ & age_ & height;
020     }

save関数は、書き込み時に呼び出されます。 気づきにくいですが、書き込みだけでクラスの状態は変化しないのでconst関数になっています。

022   template<class Archive>
023     void load(Archive& ar, const unsigned int version) 
024     {
025       if(version > 0) {
026         ar & name_ & age_ & height_;
027       }
028       else {
029         ar & name_ & age_;
030       }
031     }

load関数は、読み込み時に呼び出されます。特に前回のserialization関数と違いはありません。

「非侵入型」のシリアル化関数

さて、今までの方法では、シリアル化したいクラスにはメンバ関数を追加する必要がありました。 自分で作ったクラスならそれでもいいのですが、他人が作ったクラスやライブラリに含まれるクラスなど、 おいそれとはメンバ関数を追加できない場合も多々存在します。 今回は、そんな場合のための方策、「非侵入型(non-intrusive)」のシリアル化関数を紹介します。

では、やってみましょう。今回はWin32 SDKのPOINT構造体をシリアル化してみます。POINT構造体は、こんな感じの構造体です。

001 struct POINT {
002   LONG x;
003   LONG y;
004 };

非常に単純な構造体ですが、windows.h内で宣言されているためおいそれとは変更できません。 このPOINT構造体をシリアル化したい場合は、以下のようなコードを適当なファイルに追加します。 POINT構造体をシリアル化するすべての部分からこの関数が参照できないといけませんので、 共通ヘッダーあたりに置くのがベストだと思います。

001 namespace boost {
002   namespace serialization {
003     template <class Archive>
004       void serialize(Archive& ar, POINT& rpoint, const unsigned int version) 
005       {
006         ar & rpoint.x & rpoint.y;
007       }
008   }
009 }

細かい説明は不要でしょう。boost::serialization名前空間内に配置しなければならない点がポイントです。

これで、POINT構造体もシリアル化できるようになりました。 このような実装の実例がboost/serialization/string.hppやboost/serialization/list.hppなどにありますので、参考にするとよいでしょう。

基底クラスをシリアル化する

この項で言いたいことは一つだけです。

「基底クラスのserialize関数を直接呼び出さないこと!!」

はい、復唱。

「基底クラスのserialize関数を直接呼び出さないこと!!」

覚えましたね? 具体例を挙げて説明します。Studentクラスから派生したHighSchoolStudentクラスで考えましょう。

001 
002 class HighSchoolStudent : public Student {
003 private:
004   int grade_;
005 };

Studentクラスに学年を表すgrade_メンバ変数を加えただけのとても単純なクラスです。 これをserialization対応に書き直します。

001 
002 #include <boost/serialization/base_object.hpp>
003 
004 class HighSchoolStudent : public Student {
005 private:
006   int grade_;
007   
008   friend class boost::serialization::access;
009   template <class Archive>
010     void serialize(Archive& ar, const unsigned int version) 
011     {
012       ar & boost::serialization::base_object<Student>(*this);
013       ar & grade_;
014     }
015 };

問題となるのは、以下の部分です。

002 #include <boost/serialization/base_object.hpp>
012       ar & boost::serialization::base_object<Student>(*this);

12行目で、基底クラスのシリアル化を行なっています。 繰り返しますが、直接基底クラスのserialization関数を呼び出すのでなく、base_object関数を使用してください。 クラスのバージョン付けなどの面で不具合が発生します。 serialization関数をprivateとして宣言するのも、不用意に派生クラスから直接呼び出されないようにするための処置です。

配列をシリアライズする

配列は、そのままシリアライズ可能です。

001 class Homeroom {
002 private:
003   Student[40] students_;
004   
005   friend class boost::serialization::access;
006   template <class Archive>
007     void serialize(Archive& ar, const unsigned int version)
008     {
009       ar & students_;
010     }
011 };

なんかもう説明するまでもないくらいシンプルです。

constメンバはどうするか

constメンバは、クラスのコンストラクタでしか初期化できず、 初期化したら最後二度と変更できません。 このconstメンバをどうするかということですが……。 説明書を読むと、const_castでごり押ししろ(意訳)らしいです。こんな感じでしょうか。

001 class Student {
002 private:
003   int age_;
004   const bool sex_;
005   
006   friend class boost::serialization::access;
007   template <class Archive>
008     void serialization(Archive& ar, const unsigned int version) 
009     {
010       ar & age_;
011       ar & const_cast<bool&>(sex_);
012     }
013 };

最近は性別をconstにすると人権団体から怒られたりするんでしょうか。ちょっとどきどきです。

こういうときのためのconst_castです。 注意すべき点としては、この関数は読み書き双方に使われるので、 boolにではなくbool&型にキャストしなければならない、ということでしょうか。

解説書のこの頁に載っていた言葉が印象的だったので、引用しておきます。

Note that this violates the spirit and intention of the const keyword. const members are intialized when a class instance is constructed and not changed thereafter. However, this may be most appropriate in many cases. Ultimately, it comes down to the question about what const means in the context of serialization.

(適当な訳) この処置はconstを使用する精神と意図を破壊するものであることに留意してください。 constメンバはクラスが実体化されたときに初期化され、それ以後変更されることはありません。 しかし、このconst_castを使用する処置は多くの場合に適切です。 究極的には、これはconstというキーワードがシリアライズという文脈の中でどのような意味を持つか、 という問題に帰結します。

要するに、あんまし深く突き詰めて考えちゃだめだよってことですね。宗教戦争になりますよ、と。

ポインタからのシリアライズ:その1

単純型へのポインタや、デフォルトコンストラクタのあるクラスへのポインタの場合、 今までと同様にシリアライズが可能です。

001 class Teacher {
002 private:
003   Homeroom* phomeroom_;
004   
005   friend class boost::serialization::access;
006   template <class Archive>
007     void serialization(Archive& ar, const unsigned int version)
008     {
009       ar & phomeroom_;
010     }
011 };

保存の時にポインタの先を辿って保存してくれるのはもちろん、 読み込み時には勝手にnewしてインスタンスを作ってくれて、 複数のポインタから指されているオブジェクトも適切にシリアライズしてくれて、 さらにインスタンスが派生クラスでもちゃんと対応してくれます (それなりの手続きをしていれば、ですけど……後述)。すごい。

ポインタからのシリアライズ:その2

では、デフォルトコンストラクタのないクラスへのポインタをシリアライズしたい場合はどうするのでしょうか。 こちらは、ちょっと手順を踏む必要があります。

001 class Chair {
002 public:
003   Chair(int height) : height_(height) {}
004   int getHeight(void) const { return height_; }
005 private:
006   int height_;
007   
008   friend class boost::serialization::access;
009   template <class Archive>
010     void serialization(Archive& ar, const unsigned int version)
011     {
012       ar & height_;
013     }
014 };

デフォルトコンストラクタを持たないクラスChairについて考えます。 とりあえず、基本的なところまでは一通り書きました。

あとは、以下の関数を追加する必要があります。

001 namespace boost {
002   namespace serialize {
003     template <class Archive>
004       inline void save_construct_data (
005         Archive& ar, const Chair* p, const unsigned long int version)
006       {
007         ar << p->getHeight();
008       }
009     
010     template <class Archive>
011       inline void load_construct_data (
012         Archive& ar, Chair* p, const unsigned long int version)
013       {
014         int height;
015         ar >> height;
016         ::new(p) Chair(height);
017       }
018   }
019 }

この関数は、boost::serialize名前空間内に定義する必要がある点に注意してください。

010     template <class Archive>
011       inline void load_construct_data (
012         Archive& ar, Chair* p, const unsigned long int version)
013       {
017       }

この関数は、インスタンスのメモリは確保したけどコンストラクタはまだ呼んでいない、という段階で呼び出されます。 この関数内で、コンストラクタを呼び出し、正しくメモリ上にデータを配置する必要があるわけです。

デフォルトの実装では、デフォルトコンストラクタを呼んでいるのですが、それが存在しない場合、 このようにオーバーロードしてやる必要があるのです。

003     template <class Archive>
004       inline void save_construct_data (
005         Archive& ar, const Chair* p, const unsigned long int version)
006       {
007         ar << p->getHeight();
008       }

この関数は、load_construct_data()関数と対になる関数です。 load_construct_data()関数で必要となるデータを、アーカイブに書き出しています。

014         int height;
015         ar >> height;
016         ::new(p) Chair(height);

コンストラクタに必要な情報を読み出した上で、コンストラクタを呼び出しています。 ここで、16行目の処理は所謂「配置new(placement new)」というもので、 More Effective C++に詳しく載っているのですが、お持ちでない方はこちらのページがわかりやすいと思います。

C++ Labyrinth

ともあれ、これでデフォルトコンストラクタを持たないクラスでも ポインタを通じてシリアライズができるようになりました。バンザイ。

基底クラスへのポインタから復元する

まずは、以下のコードをご覧ください。

001 class Base {
002   ... // serialize()関数など
003 };
004 
005 class Derived : public Base {
006   ... // serialize()関数など
007 };

Baseクラスから派生したDerivedクラスがあるとします。 どっちのクラスにも、serialize()関数などシリアライズに必要な関数は一通りそろっているとして考えます。

このとき、以下のような処理は当然可能です。

001 Base* p = new Derived();
002 ar << p;

これは、前の項で説明したとおり、ポインタの指す先をたどり、 正しくDerivedクラスのserialize()関数を呼び出してくれます。

では、その保存したデータを読み出す場合はどうでしょう。

001 Base* p;
002 ar >> p;

読み出すデータは、Derived型です。serializationライブラリは、 正しくDerived::serialize()関数を呼んでくれるでしょうか?

答えから言うと、「前後の記述如何では正しく呼んでくれるかもしれないし、呼んでくれないかもしれない」です。 この読み込みが行われる時点で、serializationライブラリにDerivedクラスが「登録」されているかどうかが、 運命の分かれ道となります。

簡単に登録の仕組みを説明します。アーカイブを通して、読み出し・書き込みが行われたクラスは、 それぞれ読み書きされた順に通し番号を振られて、登録されます。 そして登録されたクラスは、 基底クラスへのポインタからの復元(ちょうど今回の例のようなケースです)ができるようになります。 逆に言えば、登録されていないクラスは、 基底クラスへのポインタからの復元はできません。

よって、上記の例が成功するかどうかは、それまでにDerivedクラスを読み書きしたことがあるかどうかにかかっているのです。

しかし、それではあまりにも不便です。 そこで、実際に読み書きを行わなくても登録ができるシステムが2つ用意されています。

register_type<T>()

text_iarchiveやbinary_oarchiveなどのアーカイブクラスには、 すべてregister_type()関数が用意されています。 この関数は、名前のとおりクラスを登録します。簡単に使い方の例を見てみましょう。

001 ar.register_type<Derived>();
002 // 上の行がコンパイル通らない場合は、こちらを試してみてください。
003 // ar.template register_type<Derived>();
004 Base* p;
005 ar >> p;

テンプレート引数Tにクラス名を渡します。 これで、Derivedクラスが登録されます。

ところでこの方式に限らないのですが、save/load関数に分割したときは、 可能な限り関数の「対称性」を保つようにしてください。 どういう意味かと言いますと、登録したときに割り振られる番号は、シーケンシャルに割り振られます。 つまり登録の順番がずれると、番号もずれてしまうのです。 書き込み/読み込みで番号のずれがあると、正しく復元をすることができません。 よって、load関数でregister_type()関数を呼び出すときは、 save関数でも同じ場所で呼び出しておく必要があります。

BOOST_CLASS_EXPORT_GUID(T, K)

もうひとつの方法が、このBOOST_CLASS_EXPORT_GUIDマクロです。 個人的にはこちらの使用をお勧めします。

001 #include <boost/serialization/export.hpp>
002 
003 class Derived : public Base {
004   ... // serialize()関数など
005 };
006 
007 BOOST_CLASS_EXPORT_GUID(Derived, "Derived");

このマクロを使うと、クラスTが登録され、 さらに通し番号の代わりに与えられた文字列Kが使用されます。 文字列は、一意なものであれば何でも構いません。 グローバルスコープで登録されるので、登録されるタイミングをいちいち考えなくてよかったり、 クラスのヘッダーに書いておけばクラスの利用者側でいちいち登録に気を使う必要がないといった利点があります。

これと似たマクロで、BOOST_CLASS_EXPORTマクロがあります。 BOOST_CLASS_EXPORT_GUIDマクロの第二引数に自動的にクラス名を入れてくれるマクロで、 BOOST_CLASS_EXPORT(Derived)のようにして使用します。

ちなみに、これらの処理を怠って、未登録の状態で基底クラスへのポインタから復元しようとした場合、 unregistered_class例外が投げられます。

実装レベル

serializationライブラリでは、デフォルトの設定ではクラスを保存する際に型情報なども同時に保存します。 これのおかげで派生クラスなどを適切にシリアライズできるのですが、 時々これではオーバースペックだと感じることがあります。

例えば、Win32SDKに含まれるRGBQUAD構造体。 派生クラスも特になく、メンバもバイト型4つだけという非常に単純な構造体です。

001 struct RGBQUAD {
002   BYTE    rgbBlue; 
003   BYTE    rgbGreen; 
004   BYTE    rgbRed; 
005   BYTE    rgbReserved; 
006 };

はたしてこの構造体をシリアライズするのに、型情報を保存する必要があるでしょうか? しかもこのクラスは、最大で256個連続して保存される可能性があるのです。 クラス情報の保存によるオーバーヘッドは莫迦になりません。

そんなときのために、serializationライブラリには「実装レベル(Implementation level)」というものが用意されています。

001 
002 #include <booost/serialization/level.hpp>
003 
004 BOOST_CLASS_IMPLEMENTATION(RGBQUAD, boost::serialization::object_class_info);

BOOST_CLASS_IMPLEMENTATIONマクロを使用することでクラスごとの実装レベルを変更することが可能です。 このマクロの第二引数で実装レベルを指定します。 実装レベルには、次の4種類があります。

not_serializable

これを指定したクラスは、シリアル化しません。 &演算子などでアーカイブに流し込まれても、無視されます。 volatile指定された場合、デフォルトでこの設定になるようです。

primitive_type

enum型や組み込み型に適用される実装レベルです。 serialize関数を通さずに、直接メモリ上のデータがアーカイブに流し込まれます。

object_serializable

serialize()関数でシリアライズしますが、型情報やバージョン情報は保存しません。 軽くしたい場合は、基本的にこれを指定しましょう。primitive_levelはコンパイラ間の互換性に疑問が残ります。

object_class_info

型情報やバージョン情報も保存します。一般クラスは、デフォルトではこの設定になっています。

オブジェクトの追跡

ポインタを通してシリアライズする場合、複数のポインタから指されているオブジェクトについて考える必要があります。 どういうことかといいますと、例えばポインタAとBから指されているオブジェクトαがあったとして、 何も考えずにAとBをシリアライズするとαが2回保存されてしまうことになります。

このような挙動を避けるため、serializationライブラリは自動的にオブジェクト追跡(Object tracking)という機能を用いて、 オブジェクトの二重保存を防いでいます。

でも当然この機能も、オーバーヘッド(主に速度・メモリ面でしょうか)と引き換えに成り立っているわけでして。 上記のような心配がない場合は、この機能も任意にオフにすることができます。

例として、先ほど挙げたRGBQUAD構造体を再び使用します。

001 
002 #include <boost/serialization/tracking.hpp>
003 
004 BOOST_CLASS_TRACKING(RGBQUAD, boost::serialization::track_never);

BOOST_CLASS_TRACKINGマクロを使用することで任意に追跡機能をオンオフできます。 第二引数で追跡機能のオンオフを指定します。指定できる値は、次の三つです。

track_never

追跡機能をオフにします。組み込み型は、デフォルトでこの設定になっています。

track_selectively

ポインタからシリアライズされた場合にのみ、追跡機能を使用します。 デフォルトではこの設定になっています。

track_always

常に追跡機能をオンにします。 いまいちメリットがよくわかりませんが……。

抽象クラスでのエラー

抽象クラスへのポインタを通して派生クラスをシリアライズしようとした場合、 コンパイルエラーが発生することがあります。 このエラーを消すためには、BOOST_IS_ABSTRACTマクロを使用します。

001 
002 #include <boost/seralization/is_abstract.hpp>
003 
004 BOOST_IS_ABSTRACT(hogehoge);

例では、hogehogeクラスが抽象クラスであるとserializationライブラリに通知しています。

NVP

アーカイブにXML形式を使用した場合、要素の値だけでなく要素名が必要となります。 そのために用意されている仕組みが「NVP」です。 std::mapみたいな要素名と値との対応付けですね。

001 
002 #include <boost/serialization/nvp.hpp>
003 
004 class HighSchoolStudent : public Student{
005 private:
006   int weight_;
007   int height_;
008   
009   friend class boost::serialization::access;
010   template <class Archive>
011     void serialization(Archive& ar, const unsigned int version) 
012     {
013       ar & boost::serialization::make_nvp("weight", weight_);
014       ar & BOOST_SERIALIZATION_NVP(height_);
015       ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(Student);
016     }
017 };

このようにして要素名と要素のペアをアーカイブに与えると、 アーカイブがXML形式のときは<要素名>要素</要素名>のように出力し、 アーカイブがXML以外の形式のときは要素名を無視します。

002 #include <boost/serialization/nvp.hpp>
013       ar & boost::serialization::make_nvp("weight", weight_);

第一引数で要素名、第二引数で要素を指定します。 要素名を自由に指定できる自由度の高い形式です。

002 #include <boost/serialization/nvp.hpp>
014       ar & BOOST_SERIALIZATION_NVP(height_);

要素名を変数名から自動的に決定してくれるマクロです。 タイプする手間を省く目的なのかもしれませんが、 boost::serializationをusingしていて変数名が11文字以内ならmake_nvp関数で指定したほうが早いです……。

002 #include <boost/serialization/nvp.hpp>
015       ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(Student);

以下の文と同義です。

000 ar & boost::serialization::make_nvp("Student", boost::serialization::base_object<Student>(*this));

基底クラスをシリアライズしつつ要素名も指定してくれます。 こちらのマクロは、ちゃんとタイプ短縮の役に立っています。

ロケールの設定

基本的には、このライブラリはロケールを設定しなくてもちゃんと動いてくれます。 が、私が調べた限りでは、1箇所だけロケールの設定を必要とする場所がありました。

001 #include <string>
002 #include <locale>
003 #include <fstream>
004 #include <boost/serialization/string.hpp>
005 #include <boost/serialization/nvp.hpp>
006 #include <boost/archive/xml_woarchive.hpp>
007 #include <boost/archive/xml_wiarchive.hpp>
008 
009 int main(void) {
010   std::locale::global(std::locale("japanese"));
011   
012   std::wofstream wofs("output.xml");
013   boost::archive::xml_woarchive oa(wofs);
014   oa << boost::serialization::make_nvp("string", std::string("日本語文字列"));
015   wofs.close();
016   
017   std::string str;
018   std::wifstream wifs("output.xml");
019   boost::archive::xml_wiarchive ia(wifs);
020   ia >> boost::serialization::make_nvp("string", str);
021   wifs.close();
022  
023   return 0;
024 }

少々長いですが、要するに「ワイド文字列アーカイブにマルチバイト文字列を流し込む」時には、 グローバルロケールがきちんと設定されている必要があるようです。

また、このケースの逆、「マルチバイト文字列アーカイブにワイド文字列を流し込む」時にもロケールの設定は必須です。