C++中使用boost::serialization库――应用篇
概要:
本文先简述在项目中使用boost::serialization库的两种实现方式:一种是成员函数模板,另一种是友元函数模板。之后涉及使用库的两种实现方式的优劣比较、使用时的代码模板示例,通过比较得出结论应以第二种实现方法为主,另外,因为boost::serialization库对任意类型的序列化代码都提供了一致的语法[像这样:ar & make_nvp("name",anyTypeVariant);],所以当用第二种友元函数模板的方法来实现所有类的序列化时,我们可以使用脚本来分析类声明文件自动产生代码,只需要在写一个类时将需要序列化的类成员变量放入一个特殊的自定义的注释标记中。本文末尾给出作者定义的标记作为参考,并附有Perl程序,用来分析所有类的声明文件,根据此标记自动生成整个项目序列化功能的代码。
总结,在项目中,结合系统级语言(如:C++)和脚本语言(如:Perl等),将部分工作进行自动代码生成,定能极大提高工作效率。
相关网站:www.boost.org www.perlchina.org
这个库:boost::serialization(以下简称BS)的优点,请参看<http://www.boost.org/libs/serialization/doc/overview.html#Requirements>。实际项目中完全可以替换MFC本身的Serialization机制,笔者最喜欢的优点在于:不给任何需要序列化的类型强加一个父类。
要使用BS的功能几乎不需要更改任何现有代码,只需要对所有要序列化的类的头文件加上一个宏声明,另外单独一个cpp实现文件放所有类的序列化模板函数,对现有项目的改动可以说微乎其微。
首先,BS是需要先编译好才能使用的,编译其实非常简单,具体参看末尾的编译boost方法。
笔者一直使用的boost_1_32_0,其中的BS已经很好用了,但只能以静态库的方式使用,最近发布的boost_1_33_0,其中的BS库可以以Dll的方式使用。
将BS相应的*.lib,*.dll的路径加入编译的相应VC目录中[工具=>选项=>项目=>VC++目录]。
需要注意的是:[项目=>属性页=>C/C++=>代码生成]中的`运行时库`一项,必须和编译的库使用一致,对应规则是:
多线程 <=> runtime-link-static/threading-multi[mt-s]
多线程dll <=> threading-multi[mt]
单线程 <=> runtime-link-static[s]
然后,需要去掉WinDef.h的min,max宏定义,这个宏会影响BS库,方法如下:
在#pragma once这行下,紧挨着加上
#define NOMINMAX
#include <algorithm>
using std::min;
using std::max;
#define BOOST_SERIALIZATION_NonIntrusiveFriendFun(class_name) template<typename A>friend void serialize(A&, class_name&, unsigned int const);
这样修改了以后,所有项目中使用min、max的地方,两个参数的类型必须一直,因模板不能在deduction时再进行类型转换,对于类型不一致的参数,使用min<int>、max<float>等等代替。
最后一个宏,是为了方便以友元函数模板方式实现类的serialize的,下面有说明。
再次,
先简要说一下,BS库的两种serialize函数的写法:
一种是作为类的成员函数来写,类的头文件声明中加上
class XYZ{
... // 类成员
friend boost::serialization::access;
template<typename A>void serialize(A &ar,unsigned int const ver)
{
// ... serialize代码
}
};
另外一种是作为类的友元函数,在类外部来实现serialize代码,类的头文件声明中加上
class XYZ{
... // 类成员
// 注意关键字`friend`和多了一个类引用作参数
template<typename A>friend void serialize(A&, XYZ&, unsigned int const);
};
然后单独一个文件放函数[template<typename A>void serialize(A&, XYZ&, unsigned int const);]的实现。
笔者极力推荐第二种友元函数实现方法,因为这种方法对项目的类之间的关系影响是最小的。如果用第一种,serialize模板函数必须放在头文件中,因为编译器进行模板实例化时使用包含模型最常见,就是使用模板函数的地方,必须能看到模板函数的定义,模板实例化的分离模型还不是主流。这样的话,如果一个类A[Aggregate]聚和了(注意不是组合)的类P[Part],类A的头文件只需要声明类P,类A头文件中不需要看到类P的定义,这样如果任何别的类使用类A,包含了类A的头文件,也只是看到了类P的声明[声明是不会改变的哦],类P的定义无论怎样修改不会影响到类A的头文件,也就不会影响使用类A的任何类UseAs,只会影响类A的.cpp文件,这个影响是很局部化的,不然对类P定义的一个小小改动,会可能影响整个项目,因为还会有别的类O[Other]使用那些使用类A的类UseAs中的某一个,而类O与类P没有任何关系,都也要受到影响。
当对项目做了一个改动就会造成整个项目到处都要做小的改动(无论是编译器做还是你手工来做),则这个项目修改维护的代价就是不可预知的,这样的项目可以说是很混乱的,没有任何内聚性、模块化可言,耦合非常大。
尽量优先使用聚和,然后才是组合,就是为了降低耦合。
用第二种方法,还有一个很大的好处,就是不需要写任何类的serialize友元函数的代码,因为BS的语法是对任何变量都是一致的,无论是基本类型、指针、多态指针、对象、职能指针、容器……,无论是以文本文件、二进制文件、XML格式存取,均是一致的写法(ar & make_nvp("name",value);),这就必然导致了序列化代码自动生成,因为这已经变成一个体力活了,看过《程序员修炼之道》的话,项目做一段时间,就应该有自己的代码生成器了。
基本上项目中使用BS的代码是这个情形,有一个最高级别的总管类CRoot,其中一个函数有一个文件名作为参数,另外提供一个bool参数表示存或取,函数声明如下:void SerializeByFile(std::string const&, bool);
实现就放在一个单独的文件中,(特别注意)不要和其他的CRoot成员函数放一起。因为模板实例化很费时间,CRoot的其它成员函数改变,就把所有类的serialize模板重编译一遍是很划不来的,特别是项目大,要序列化的类很多时,也可能会发生[VC的编译器内部错误,堆空间不足],不过不用担心,一般不会的。 :)
单独的CRoot::SerializeByFile实现文件内容格式如下:
------------------------------------------------
#include "stdafx.h" // 预编译头
#include "Root.h" // 类自身的定义
#include "autoGenerateSerailizeImp.h" // 自动生成的所有类的serialize模板函数
// 如下是CRoot的SerializeByFile代码的一般写法,这个函数中的代码导致编译器实例化所有类(包含CRoot)的序列化模板函数
void CRoot::SerializeByFile(std::string const& fileName, bool bSave)
{
using namespace std;
using namespace boost::serialization;
if (bStore)
{
std::ofstream ofs(fileName.data(),ios::binary);
boost::archive::binary_oarchive oa(ofs); // 注意 ios::binary 在Windows平台必须有
// 类型注册模板函数,由<autoGenerateSerailizeImp.h>文件提供
auto_register_type(oa);
oa & make_nvp("root_data",*this);
}
else
{
std::ifstream ifs(fileName.data(),ios::binary);
boost::archive::binary_iarchive ia(ifs);
auto_register_type(ia);
ia & make_nvp("root_data",*this);
}
}
下面给出任何类[如:class CAny{any_type a;……};]的友元函数模板实现:template<typename A>void serialize(A &ar, CAny &cls, unsigned int const ver)
{
ar & boost::serialization::make_nvp(cls.a);
……
}
BS库的这种对任何类的任何类型的成员变量的一致写法,导致我们完全可以不去写serialize友元函数模板,只需要把需要serialization的成员变量,在类中定义时就用一个自定义的标记指出就行了,然后由程序自动搜索所有头文件,根据标记产生<autoGenerateSerailizeImp.h>文件即可,此文件内有自动生成的所有类的序列化模板函数。
下面给笔者定义的标记:
/*! 请将类中需要serialization的成员变量放入如下格式的标签段落内,全大写的字符串请根据实际情况做替换,[]内为可选项
BOOST_SERIALIZATION_NonIntrusiveFriendFun(CLASS_NAME)
///<serialization-CLASS_NAME[-BASE_CLASS]>
...
SERIALIZABLE_DATA_MEMBER
...
///</serialization>
*/
例如:
class CBase
{
int bb; //非序列化成员
BOOST_SERIALIZATION_NonIntrusiveFriendFun(CBase) // 注意这里的`CLASS_NAME`被替换成CBase
///<serialization-CBase> // 注意这里的`CLASS_NAME`被替换成CBase
float f; // 所有需要序列化的成员放在这个标记区段内,成员可以像这样也加上注释
///</serialization>
}
class CDerive : public CBase
{
int dd; //非序列化成员
BOOST_SERIALIZATION_NonIntrusiveFriendFun(CDerive) // 注意这里的`CLASS_NAME`被替换成CDerive
///<serialization-CDerive-CBase> // 注意这里的`CLASS_NAME`被替换成CDerive,`BASE_CLASS`被换为CBase
float other_data;
///</serialization>
}
最后给出Perl代码来自动生成<autoGenerateSerailizeImp.h>文件,当然你可以用任何喜欢的语言(包括最爱的C++)来做这项分析头文件的体力活。现在的代码还不能到达足够智能的地步,理想的情况是类名、命名空间、类之间的继承关系都应该自动分析出来才是最好的,这样也就具有了反向工程生成类图的基础了,留给读者自己完善吧!:)
//! 这里给出Perl程序自动产生文件<autoGenerateSerailizeImp.h>,此文件提供序列化所有类的功能
//! -----------------------------------------------------------------------
#! perl -w
use strict;
use warnings;
#这里一定要给出项目路径,如: D:/Project/useBoostSerialization,末尾不用加'/'
my $myPath='ProjectFullPath';
my $output_file = 'autoGenerateSerailizeImp.h';
#-------------Info--------------------
open (F,">$myPath/$output_file") || die("Can't open file $output_file:$!\n");
print F "\n//!Generated on ",scalar(localtime())," For Project By MSLK PERL","\n"x2;auto_default(\*F);close F;
#-------------------------------------
my $tag_beg = '///<serialization-CLASS_NAME-BASE_CLASS>'; # include class_name,future include namespace...
my $tag_end = '///</serialization>';
my $fileVer1 = "BOOST_CLASS_VERSION("; my $fileVer2=",\t\t0);";
my $funPart1 = "template <typename Archive>\nvoid serialize(Archive &ar, ";
my $funPart2 = " &cls, unsigned int const file_version)\n{\n\tusing namespace boost::serialization;\n\n";
my $funBO1 = "\tar & make_nvp(\"base_object\", base_object<";my $funBO2 = ">(cls));\t///base_class\n";
my
$fun_register_type1 = "//! register polymorphic
type\ntemplate<typename Archive>\nvoid auto_register_type(Archive
&ar)\n{\n";
#-------------------------------------
my %SuperSubClass_TypeInfo; # className=>arrayDerivedClassName
my $ssti=\%SuperSubClass_TypeInfo; #简写
my $curClassName;
#-------------------------------------
sub ConcreteThisFile
{
my ($f) = @_;return if($f !~ /.*\.h$/);
open(F,"$f") || die("Can't open source file :$f\n");
open(outF,">>$myPath/$output_file") || die("Can't write output file :$output_file\n");
my $inKeyDomain = 0;
while(<F>)
{
$inKeyDomain=0,print outF "}\n\n\n"if(/.*\/\/\/\<\/serialization\>/);
if($inKeyDomain){print outF "\tar & make_nvp(\"$1\", cls.$1);\t\t$2\n"if(/.*\s(\w*)\s*;\s*(\/?\/?.*)/)}
$inKeyDomain=1,getIncludeFunInfo(\*outF,$f,$1,$2)if(/.*\/\/\/\<serialization-([\w:]+)-?([\w:]+)?\>/)
}
close F;close outF;
}
sub getIncludeFunInfo
{
my $F=$_[0];
print $F "#include \"./",substr($_[1],length($myPath)+1),"\"\n$fileVer1$_[2]$fileVer2\n$funPart1$_[2]$funPart2";
$ssti->{$_[2]} = [] unless(exists($ssti->{$_[2]}));
(exists($ssti->{$_[3]})||($ssti->{$_[3]}=[],0)),push(@{$ssti->{$_[3]}},$_[2]),print $F "$funBO1$_[3]$funBO2\n"if($_[3]);
}
sub recursiveScanDir
{
my ($cur_root) = @_;
opendir(D,$cur_root);
my @dir = readdir(D);
foreach(@dir)
{
if ( $_ !~ /^\.{1,2}$/) # exclusion <.><..>
{
$_=$cur_root.'/'.$_;recursiveScanDir($_),next if(-d);ConcreteThisFile($_),next if(-f);
print "Can't process this file: $_\n";
}
}
}recursiveScanDir($myPath);
sub create__register_type
{
open(outF,">>$myPath/$output_file") || die("Can't write output file :$output_file\n");
print outF $fun_register_type1;
for(sort keys(%$ssti)){print outF "\tar.register_type( static_cast<$_*>(NULL));\n" unless(scalar(@{$ssti->{$_}}));}
print outF "\n}\n";
close outF;
}create__register_type();
#
sub auto_default
{
my ($F) = (@_);
print $F '
#pragma warning(disable:4244)
#pragma warning(disable:4267)
#include <iostream>
#include <fstream>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/serialization/serialization.hpp>
#include <boost/serialization/shared_ptr.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/serialization/list.hpp>
#include <boost/serialization/utility.hpp>
#include <boost/serialization/string.hpp>
#include <boost/serialization/map.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/is_abstract.hpp>
';
print $F '
//! 如有其他MFC类型也需要像这样给出友元函数模板来序列化
template<typename A>inline void save(A &ar, CString const&cs, unsigned int const version){std::wstring wstr(cs);ar & BOOST_SERIALIZATION_NVP(wstr);}
template<typename A>inline void load(A &ar, CString &cs, unsigned int const version){std::wstring wstr;ar & BOOST_SERIALIZATION_NVP(wstr);cs=wstr.data();}
template<typename A>inline void serialize(A &ar, CString &cs, unsigned int const version){boost::serialization::split_free(ar, cs, version);}
';
print $F "\n";
}
//! -----------------------------------------------------------------------
编译boost库方法:
这里简单说一下,boost支持各种常用C++编译器,我们要做的修改就是指出编译器的路径在哪里。
但boost库的编译不使用make程序,而是bjam。bjam的源代码在boost库的boost_1_3*_0/tools/build/jam_src中,首先要编译出bjam.exe程序,找到boost_1_3*_0/tools/build/jam_src/build.bat文件编辑打开,搜索`Microsoft`[或者你C++编译器的特有字符串],
找到如下行: if EXIST "%ProgramFiles%\Microsoft Visual Studio .NET 2003\VC7\bin\VCVARS32.BAT" (
将引号里的内容换成自己本地的路径,比如: if EXIST "D:\VsNet2003\Vc7\bin\VCVARS32.BAT" (
还有接着的第二行: set BOOST_JAM_TOOLSET_ROOT=%ProgramFiles%\Microsoft Visual Studio .NET 2003\VC7也要换掉,比如: set BOOST_JAM_TOOLSET_ROOT=D:\VsNet2003\Vc7\bin\VC7
这两处换成自己的编译器路径,就可以运行boost_1_3*_0/tools/build/jam_src/build.bat了,编译好的bjam.exe,
在boost_1_3*_0\tools\build\jam_src\bin.ntx86目录下,copy到boost_1_3*_0/目录下,方便使用。
下面,就可以用bjam.exe编译boost的库了。还要先修改一个路径,打开boost_1_3*_0\tools\build\v1\vc-7_1-tools.jam文件,修改
VC71_ROOT ?= $(ProgramFiles:J=" ")"\\Microsoft Visual Studio .NET 2003\\VC7" ;
这一行,比如(注意[?=][=]): VC71_ROOT = "D:\Vsnet2003\Vc7" ;
好了,下面打开命令提示符,到boost_1_3*_0/目录下,先执行*\Vc7\bin\vcvars32.bat设置VC环境变量,再输入如下命令:
bjam
编译boost库。
最后,总结几个注意事项:
1.去掉WinDef.h的min,max宏定义
2.运行时库要一致
3.binary_*archive的fstream必须用iso::binary方式创建
4.Debug版本的内存管理需要注意,CObject重载了new操作符跟踪内存泄漏,而用BS从文件产生的对象等同于是用::new产生的,删除时请用::delete。