如果再看会有后续,主要是翻译(整理了网上几位大侠有关序列化指南的翻译)和一些例子(文档里的和自己测的),模版类和万能的泛型算法不太懂。
BOOST序列化指南
1总述
1.1简介
这里,我们用术语序列化(serialization)来表示将一组原始的C++数据结构表示为字节流达到可逆析构的目的。这样的系统可以用来在另一个程序环境中重新建立原来的数据结构。因此,它也可以作为对象持久性(object persistence),远程参数传递(remote parameter passing),或者其他特性的实现基础。在我们的系统中,将使用术语档案(archive)表示一个具体的字节流。档案可以是二进制文件,文本文件,XML文件,或者其他用户定义的类型。
1.2目标
1. 代码的可移植性--只依靠ANSI C++的特性。
2. 代码的经济性--挖掘各种C++的特性如RTTI、模板、和多继承等等使用户容易使用并且代码短小。
3. 类版本的独立性。--当一个类的定义改变时,老版本的类的档案仍然可以被导入新版本的类中。
4. 指针的深度存储和恢复。--保存或恢复指针的同时保存或恢复指针指向的数据。
5. 正确的处理多个指针指向相同对象时的问题。
6. 对STL和其他常用模板类的序列化的直接支持。
7. 数据的可移植性--在一个平台上建立的字节流在另一个平台上也应该是正确的。
8. 序列化和档案格式的正交性--可以在不改变类的序列化部分时应用任何格式的文件作为档案。
9. 支持非侵入(Non-intrusive)式的实现。类不需要从某个特定的类派生或者实现特定的成员函数。这对于我们不能或不愿意修改类的定义的情况时是相当必要的。
10. 档案的接口应该足够简单使建立新类型的档案的工作变得轻松。
11. 档案应该支持XML格式。
1.3其他实现方案
在开始这项工作之前,我找了一些目前的实现方案
MFC 是我非常熟悉的一个实现方案。我已经使用它数年了,而且发现它非常的有用。然而它不符合需求1,2,3,6,7,9。尽管如此,这人是我发现的最有用的实现。另外我发现类版本--MFC中部分的实现了这个特性--的支持在我的程序中是不可或缺的。比如,版本1.x的运输程序常常存储比以前提供的数据更多的信息。MFC是唯一支持版本功能的实现方案--尽管只能用于最晚辈派生类(most derived class)。但有总比没有好。另外MFC不支持STL容器的序列化,它只为MFC容器服务。
CommonC++ libraries [1] 这相当接近MFC的实现而且也解决了部分MFC的问题。它是可移植的,它建立可移植的档案但是不支持版本功能。它正确的处理了执政的存储,并且支持STL容器。它也解决了档案的压缩(虽然我不喜欢她的实现方式)。但这个库需要一个更好的文档。它不符合需求2,3,7,8,9。
Eternity [2] 这是一个"裸库":-)。它的代码很漂亮但是它确实需要一份好的文档和更多的例子。没有充分的学习他的源代码的话你讲不知道如何去使用它。最新的版本支持XML格式。它不符合需求3,6,7?,8,9。
Holub's implementation [3] 它第一个让我认真思考我自己对序列化的需求的实现。你如果不是抱着自大和傲慢的态度的话,读它将是相当有趣和值得的。它不符合需求2,3,4,5,6。
s11n [13] 这个库的目标和我们相当接近。一些方面的实现也相当类似。在写这篇文章之时,它的一些问题是:
代码的可移植性(1)。 代码只在最近的GCC版本中可以使用。
类版本的独立性(3)。 类版本不被直接支持。
需求(5).它并没有自动的处理多指针问题。我还从文档中总结出来向图一样的数据结构也不支持。
它与我们的实现有许多不同也有许多相同的地方。
2使用指南
概念定义:
SA代表一个输出档案
LA代表一个输入档案
T代表要序列化的类型
ar << data;ar & data;
一个输出档案和一个输出流非常像。数据用<<或者&操作符可以存入档案。
ar >> data;ar & data;
一个输入档案和输出流也很相似。用>>和&操作符可以从档案中获得数据。
当这些操作符用于基本数据类型时,数据简单的存储进档案/从档案中载入。当用于类类型数据时,类的serialize函数就被调用。每个serialize 函数都通过上面的那些操作符来存储和载入类的数据成员。这是一个递归的过程,直到所有的所有的存储/载入工作都已经完成。
2.1可以进行序列化的类型
原语类型:
这里原语类型的意思是数据简单地存入档案,或者从档案取出,没有更进一步的处理过
程。包括算术(int 、float等,包括char)、bool、enum、stl::string、stl::wstring等。可以理解为基本类型,即占有连续内存的类型。其中string有点特殊,虽然不是连续内存,但序列化库经过专门处理,可把它当作基本类型使用。
类类型:
自定义的类类型,如果要序列化,必须把序列化库声明为自己的友元,定义了一个成员函数serialize(),或者定义一个全局函数serialize()。
指针:
指针必须指向可序列化的类型。
引用:
引用必须是可序列化的类型的引用。
数组或者容器:
数组或者容器的元素必须是可以序列化的类型。
内存块
一段有起始地址和长度的内存块
2.2原语类型
输出:
SA sa;
T data;
sa << data;或者sa & data;
输入:
LA la;
T data;
la >> data;或者la & data;
2.3自定义类型:侵入(Intrusive)方式
<<和&操作符在 serialize 函数内部用来存储和载入类的数据成员。
demo.cpp 演示了如何使用我们的系统。下面摘录的代码用最简单的例子演示这个库应该被如何使用。代码:
#include <fstream>
// include headers that implement a archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
/////////////////////////////////////////////////////////////
// gps coordinate
//
// illustrates serialization for a simple type
//
class gps_position
{
private:
friend class boost::serialization::access;
// When the class Archive corresponds to an output archive, the
// & operator is defined similar to <<. Likewise, when the class Archive
// is a type of input archive the & operator is defined similar to >>.
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & degrees;
ar & minutes;
ar & seconds;
}
int degrees;
int minutes;
float seconds;
public:
gps_position(){};
gps_position(int d, int m, float s) :
degrees(d), minutes(m), seconds(s)
{}
};
int main() {
// create and open a character archive for output
std::ofstream ofs("filename");
// create class instance
const gps_position g(35, 59, 24.567f);
// save data to archive
{
boost::archive::text_oarchive oa(ofs);
// write class instance to archive
oa << g;
// archive and stream closed when destructors are called
}
// ... some time later restore the class instance to its orginal state
gps_position newg;
{
// create and open an archive for input
std::ifstream ifs("filename", std::ios::binary);
boost::archive::text_iarchive ia(ifs);
// read class state from archive
ia >> newg;
// archive and stream closed when destructors are called
}
return 0;
}
任何想通过序列化存储的类都必须有一个函数存储它的成员以表示这个类的状态。相应的,想从序列中获得数据的类必须定义一个函数,这个函数以和存储的次序相同的顺序获得数据。在上面的例子中,这个函数以一个成员模板函数 serialize的形式出现。
注意点:
1).存取次序
存数据和取数据的次序必须一致,类型必须相同
2).操作符“&”
用输出档案时操作符“&”被定义成“<<”,用输入档案时操作符“&”被定义成“>>”,这样做有两个好处。一、写serialize()函数只用写一份;二、输出和输入档案调用同一个serialize()函数,保证了存取次序的一致性。
2.4自定义类型:非侵入方式
上面的例子用了侵入式的表达方式,也就是类的定义必须为了序列化的要求而作相应的更改。有些情况下这个可能不方便。这个系统中还有一个等价的表达方式:
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
class gps_position
{
public:
int degrees;
int minutes;
float seconds;
gps_position(){};
gps_position(int d, int m, float s) :
degrees(d), minutes(m), seconds(s)
{}
};
namespace boost {
namespace serialization {
template<class Archive>
void serialize(Archive & ar, gps_position & g, const unsigned int version)
{
ar & g.degrees;
ar & g.minutes;
ar & g.seconds;
}
} // namespace serialization
} // namespace boost
这个例子中serialize函数不再是成员函数。但工作方式几乎和上面的例子中的函数一样。
非侵入主要用于无法改变类的定义又想加入序列化支持的情况下。为了达到这个目的,类必须提供足够的信息和接口来重建类的状态。在这个例子中,我们简单的使用了公有数据成员——这当然很不常见。只有当类提供足够的信息和接口存储和载入类的状态时才能在不改变类的定义的情况下加入序列化的支持。
2.5自定义类型:可序列化的成员
一个有可序列化成员的可序列化类:
class bus_stop
{
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & latitude;
ar & longitude;
}
gps_position latitude;
gps_position longitude;
protected:
bus_stop(const gps_position & lat_, const gps_position & long_) :
latitude(lat_), longitude(long_)
{}
public:
bus_stop(){}
// See item # 14 in Effective C++ by Scott Meyers.
// re non-virtual destructors in base classes.
virtual ~bus_stop(){}
};
可以看到,可序列化成员的序列化方式和基本数据类型没有什么区别。
注意到存储一个bus_stop类也会调用latitude和longitude的 serialize 函数,它在gps_position中定义。 这种方式允许整个数据结构只要通过存储他们的根就可以将所有正确的序列化。
2.6自定义类型:派生类
派生类应该负责调用它们的基类的序列化函数。
#include <boost/serialization/base_object.hpp>
class bus_stop_corner : public bus_stop
{
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
// serialize base class information
ar & boost::serialization::base_object<bus_stop>(*this);
ar & street1;
ar & street2;
}
std::string street1;
std::string street2;
virtual std::string description() const
{
return street1 + " and " + street2;
}
public:
bus_stop_corner(){}
bus_stop_corner(const gps_position & lat_, const gps_position & long_,
const std::string & s1_, const std::string & s2_
) :
bus_stop(lat_, long_), street1(s1_), street2(s2_)
{}
};
注意派生类是如何调用基类的序列化函数的。千万不要直接调用基类的serialize 函数。这样做看上去似乎是可以工作,但是这样会绕过用来排除冗余数据的代码,而且会绕过版本机制。基于这个原因,建议总是将 serialize 设置为私有成员函数。语句friend boost::serialization::access 会保证serialization library 可以存取类的私有成员和调用类的私有函数。
2.7指针
2.7.1普通的例子
假设我们现在定义一个bus stops的数组作为bus route。 考虑到:
1、我们可能有几种不同的bus stops(bus_stop作为基类)
2、一个特定的bus_stop可能不只出现在一个线路中。
用一个bus_stop指针数组来表示一个bus route是很自然的方式。
class bus_route
{
friend class boost::serialization::access;
bus_stop * stops[10];
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
int i;
for(i = 0; i < 10; ++i)
ar & stops[i];
}
public:
bus_route(){}
};
stops数组的每一个元素都会被序列化。但是,记住它的每一个元素都是一个指针——这又意味着什么呢?指针指向的对象可以在下一次通过序列化重新构造时在另一个地址生存。为了完成一个指针的序列化,仅仅存储指针的值是不够的,它所指向的对象同样也必须被存储。当下一次指针成员重新被载入时,它所指向的对象必须也已经被建立好了,而只是指针就指向这个新的对象。
所有这些都由我们的库自动地完成了。以上的代码就是所有你想正确的序列化一个指针时所要做的工作。
2.7.2注意事项
2.7.2.1任何类型的指针不能直接 ar & p
例子1:
int *p = new int[4];
ar & ip;//编译不通过
例子2:
int i = 86;
int *p = &i;
ar & ip;//编译不通过
例子3:
void test(int array[], int *i)
{
ar & array;//编译不通过
ar & i;//编译不通过
}
2.7.2.2指针的序列化方法
可以通过2.7.1中普通例子的方法序列化指针,泛化后为
例子:
type name[size]; //type可以为指针或其他基本类型
ar & name;//正确
容器里存指针,待测试
2.7.2.3指针的反序列化
从档案中读取指针(反序列化),在使用完毕后要调用函数delete_created_pointers(),否则会内存泄漏。
SA sa;
LA la;
例子1:
写:
gps_position g1(11, 11, 11.567f);
gps_position g2(22, 22, 22.567f);
gps_position* gArray[2] = {&g1, &g2};
sa & gArray;
读:
gps_position* gArray[2] = {0};
la & gArray;
la.delete_created_pointers();
说明:数组里的对象是动态申请的,需调用delete_created_pointers()函数释放内存。
例子2:
写:
gps_position g1(11, 11, 11.567f);
gps_position g2(22, 22, 22.567f);
gps_position *gArray[2] = {&g1, &g2};
sa & g1;
sa & gArray;
读:
gps_position g1;
gps_position * gArray[2] = {0};
la & g1;
la & gArray;
la.delete_created_pointers();//数组里的对象是动态申请的,需释放
说明:此时&g1和gArray[0]相等,即指向同一块内存,内存块的生命周期由g1决定,不需delete gArray[0],只需delete gArray[1],如果delete gArray[0],程序断言终止。当然调用delete_created_pointers()则序列化库会自动判断。
例子3:运行时异常
写:
gps_position g1(11, 11, 11.567f);
gps_position g2(22, 22, 22.567f);
gps_position *gArray[2] = {&g1, &g2};
sa & gArray;
sa & g1;
读:
gps_position g1;
gps_position * gArray[2] = {0};
la & gArray;
la & g1;
la.delete_created_pointers();//数组里的对象是动态申请的,需释放
说明:如果序列化一个变量和该变量的指针,则应该先序列化变量后序列化指针,并且反序列化变量和指针后,不能delete该变量的指针,其生命周期由该变量决定
2.8内存块
save_binary(const void *address, std::size_t count)
从地址address开始取count个字节存到档案里
load_binary(void *address, std::size_t count)
从档案里取count个字节存到以地址address开始的内存块
3概念
3.1档案的概念
3.1.1档案的类型
// a portable text archive
boost::archive::text_oarchive // saving
boost::archive::text_iarchive // loading
// a portable text archive using a wide character stream
boost::archive::text_woarchive // saving
boost::archive::text_wiarchive // loading
// a non-portable native binary archive
boost::archive::binary_oarchive // saving
boost::archive::binary_iarchive // loading
// a portable XML archive
boost::archive::xml_oarchive // saving
boost::archive::xml_iarchive // loading
// a portable XML archive which uses wide characters - use for utf-8 output
boost::archive::xml_woarchive // saving
boost::archive::xml_wiarchive // loading
3.1.2档案的构造函数
iarchive(IStream & is, unsigned int flags = 0)。输出档案和输入档案类似。
1).is
is必须是basic_istream<char>或者其派生类的实例
2).flags
namespace boost {
namespace archive {
enum archive_flags {
no_header = 1, // suppress archive header info
no_codecvt = 2, // suppress alteration of codecvt facet
no_xml_tag_checking = 4 // suppress checking of xml tags - igored on saving
};
} // archive
} // boost
一般不需要关心,使用默认值。
3.1.3档案的析构函数
~iarchive()。输出档案和输入档案类似
要在stream被关闭之前调用档案的析构函数,因为析构函数会把stream恢复到档案被打开之前的状态,即档案析构的时候会调用stream的函数。一般不用关心。
4小结
4.1参考
主要是BOOST序列化库文档(http://www.boost.org/libs/serialization/doc)的翻译(整理了网上几位大侠的翻译),加上了自己测试的一些例子。可能有很多理解上不对的地方,以序列化文档和测试为准。
4.2问题
未看到、未提及、未发现的问题待以后整理。
1).序列化是否解决字节序问题。测试。
2).序列化实现的原理和类库的模板结构。了解

