前言:
此系列文章原文为boost官网的Serialization的教程。
翻译方式为有道翻译+本人人工。
编写目的是能更方便的理解使用boost的序列化。
因篇幅问题,分多个文章进行翻译。
如果有问题,欢迎指正。
文章中标红部分,为原文链接处。
------------------------------------------------------------分割线------------------------------------------------------------
目录
demo_xml_save.cpp 和demo_xml_load.cpp
前言
术语“序列化”表示将任意一组c++的数据结构可逆转换为字节序列。使用字节序列的好处是方便实现对象持久化、远程参数传递或其他功能。在本系统中,使用术语“存档”来表示这个字节流的特定呈现。该存档方式可以是:二进制数据、文本数据、xml或者由该库的用户创建的其他文件。
系统目标
- 代码的可移植性 -- 仅依赖于ANSI C++工具。
- 代码的经济性 -- 利用c++的特性,例如 RTTI、模板和多重继承等。可以使得代码更短使用更简单。
- 对每个类定义进行独立的版本控制。即,当一个类的定义改变了,旧版本文件仍然可以导入到类的新版本中。
- 深指针的存储和恢复。即指针的保存和恢复保存和恢复所指向的数据。
- 正确的恢复共享数据的指针。
- STL容器和其他常用模板的序列化。
- 数据的可移植性 -- 一个平台创建的字节流应该可以被其他任何平台读取。
- 类序列化和存档形式的正交规范。即,任何文件格式都应该能够存储任意的一组c++数据结构的序列化,并且不需要对任何类的序列化做出改变。
- 非侵入性的。允许序列化应用于未被修改的类。即,不要求序列化的类派生自特定的基类或实现指定的成员函数。这对于方便地允许序列化应用于我们不能或不希望修改的类库中的类是必要的。
- 存档的接口必须足够简单,以方便创建新的存档类型。
- 归档接口必须足够丰富,以允许创建以有用的方式将序列化数据表示为XML的归档。
其他实现
- MFC :十分有用,但他不符合目标的1,2,3,6,7,9。
- 常见的c++库:可移植,创建了可移植的存档,但是跳过了版本控制。支持正确和完整的恢复指针和STL集合。不符合目标的2,3,7,8,9。
- Eternity:是一个简单的包。需要文档和示例来研究怎么使用。不符合目标的3,6,7,8,9。
- Holub's implementation:是一篇关于序列化的文章。
- S11n:一个和boost/Serilization 很相似的库
教程
输出存档类似于输出数据流。数据可以保存到存档,通过 << 或者 & 操作符:
ar << data;
ar & data;
输入存档类似于输入数据流。可以使用 << 或者是& 操作符加载存档中的数据。
ar >> data;
ar & data;
当这些操作符被用于基本数据类型时,数据被简单地保存/加载到/从存档文件中。当为类数据类型调用时,将调用类序列化函数。每个序列化函数都使用上述操作符保存/加载其数据成员。这个过程将以递归的方式继续进行,直到保存/加载类中包含的所有数据。
一个非常简单的例子
这些操作符在序列化函数中用于保存和加载类数据成员。
这个库中包含一个名为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");
boost::archive::text_iarchive ia(ifs);
// read class state from archive
ia >> newg;
// archive and stream closed when destructors are called
}
return 0;
}
对于通过序列化保存的每个类,必须存在一个函数来保存定义类状态的所有类成员。对于要通过序列化加载的每个类,必须存在一个函数,以保存这些类成员时的相同顺序加载这些类成员。在上述示例中,这些函数是由模板成员函数serialize生成的。
非侵入性的版本
上述公式具有侵入性。也就是说,它要求更改其实例要序列化的类。在某些情况下,这可能很不方便。联合国系统允许的一种相当的替代提法是:
#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
在这个例子中,生成的序列化函数不是gps_position类的成员。这两个公式的作用方式完全相同。
非侵入式序列化的主要应用是允许在不改变类定义的情况下为类实现序列化。为了实现这一点,类必须公开足够的信息来重建类状态。在这个例子中,我们假设类有公共成员——这并不常见。只有公开了足够的信息以保存和恢复类状态的类才可以序列化,而无需更改类定义。
可序列化的成员
具有可序列化成员的可序列化类应该是这样的:
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的序列化函数。然后,通过在gps_position的定义中调用serialize来保存这些变量。通过这种方式,存档操作符的应用程序将整个数据结构保存到其根项。
派生类
派生类应该包含其基类的序列化。
#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_)
{}
};
注意从派生类序列化基类。不要直接调用基类的序列化函数。这样做似乎是可行的,但将绕过跟踪写入存储的实例以消除冗余的代码。它还将绕过将类版本信息写入存档。因此,建议始终将成员序列化函数设为私有。声明friend boost::serialization::access将授予序列化库对私有成员变量和函数的访问权。
指针
假设我们将公交路线定义为一个公交站点数组。考虑到:
- 我们可能有几种类型的公共汽车站(记住bus_stop是一个基类)
- 一个给定的公交车站可能出现在多条线路上。
用一个指向bus_stop的指针数组来表示公交线路线是很方便的。
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(){}
};
数组的每个成员都将被序列化。但是记住每个成员都是一个指针——那么这到底意味着什么呢?这种序列化的全部目的是允许在另一个地点和时间重构原始数据结构。为了用指针实现这一点,仅仅保存指针的值是不够的,而是必须保存它所指向的对象。当稍后加载成员时,必须创建一个新的对象,并且必须将一个新的指针加载到类成员中。
如果同一指针被序列化多次,则只会将一个实例添加到归档文件中。当读回时,没有数据被读回。发生的唯一操作是将第二个指针设置为与第一个指针相等。
注意,在这个例子中,数组由多态指针组成。也就是说,每个数组元素指向几种可能类型的公交车站之一。因此,当保存指针时,必须保存某种类型的类标识符。加载指针时,必须读取类标识符,并构造相应类的实例。最后,可以将数据加载到新创建的正确类型的实例中。从demo.cpp中可以看出,通过基类指针序列化指向派生类的指针可能需要显式枚举要序列化的派生类。这被称为“注册”或“导出”派生类。文中详细说明了这一要求以及满足这一要求的方法。
数组
上述公式实际上比必要的要复杂得多。序列化库检测被序列化的对象是否为数组,并发出与上述代码等价的代码。所以以上可以缩写为:
class bus_route
{
friend class boost::serialization::access;
bus_stop * stops[10];
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & stops;
}
public:
bus_route(){}
};
STL 集合
上面的例子使用了一个成员数组。更有可能的是,这样的应用程序会使用STL集合来实现这样的目的。序列化库包含用于序列化所有STL类的代码。因此,下面的重新表述也将如你所期望的那样工作。
#include <boost/serialization/list.hpp>
class bus_route
{
friend class boost::serialization::access;
std::list<bus_stop *> stops;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & stops;
}
public:
bus_route(){}
};
类版本控制
假设我们对bus_route类感到满意,构建一个使用它的程序并交付产品。一段时间后,决定程序需要增强,并修改bus_route类以包含线路上司机的名称。新版本就像这样:
#include <boost/serialization/list.hpp>
#include <boost/serialization/string.hpp>
class bus_route
{
friend class boost::serialization::access;
std::list<bus_stop *> stops;
std::string driver_name;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & driver_name;
ar & stops;
}
public:
bus_route(){}
};
成功添加了 司机的名称,但是对于使用修改之前的版本并创建了一堆文件的程序来说,如何在新程序中使用这些?
通常,序列化库在归档文件中为每个序列化的类存储一个版本号。默认情况下,此版本号为0。加载存档时,将读取存档保存时所用的版本号。可以修改上面的代码来利用这一点。
class bus_route
{
friend class boost::serialization::access;
std::list<bus_stop *> stops;
std::string driver_name;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
// only save/load driver_name for newer archives
if(version > 0)
ar & driver_name;
ar & stops;
}
public:
bus_route(){}
};
BOOST_CLASS_VERSION(bus_route, 1)
通过对每个类的版本控制应用程序,不需要尝试维护文件的版本控制。也就是说,文件版本是其所有组成类的版本的组合。这个系统允许程序总是与以前所有程序版本所创建的归档文件兼容,而不需要比本例所要求的更多的努力。
将序列化拆分为save/load
序列化函数简单、简洁,并保证以相同的顺序保存和加载类成员——这是序列化系统的关键。然而,在有些情况下,加载和保存操作与这里使用的示例不同。例如,这可能发生在经过多个版本演化的类上。上面的类可以重新表述为:
#include <boost/serialization/list.hpp>
#include <boost/serialization/string.hpp>
#include <boost/serialization/version.hpp>
#include <boost/serialization/split_member.hpp>
class bus_route
{
friend class boost::serialization::access;
std::list<bus_stop *> stops;
std::string driver_name;
template<class Archive>
void save(Archive & ar, const unsigned int version) const
{
// note, version is always the latest when saving
ar & driver_name;
ar & stops;
}
template<class Archive>
void load(Archive & ar, const unsigned int version)
{
if(version > 0)
ar & driver_name;
ar & stops;
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
public:
bus_route(){}
};
BOOST_CLASS_VERSION(bus_route, 1)
宏BOOST_SERIALIZATION_SPLIT_MEMBER()生成调用save或load的代码,具体取决于存档是用于保存还是加载。
存档
我们在这里讨论的重点是向类添加序列化功能。要序列化的数据的实际呈现是在archive 类中实现的。因此,序列化的数据流是类和所选存档的序列化的产物。这两个组件是独立的,这是一个关键的设计决策。这允许任何序列化规范都可以用于任何存档。
在本教程中,我们使用了一个特定的存档类—text_oarchive用于保存,text_iarchive用于加载。文本存档将数据呈现为文本,并且可以跨平台移植。除了文本存档外,这个库还包括用于本地二进制数据和xml格式数据的存档类。所有存档类的接口都是相同的。一旦为一个类定义了序列化,这个类就可以序列化为任何类型的存档。
如果当前archive 类不提供特定应用程序所需的属性、格式或行为,则可以创建一个新的归档类或从现有归档类派生。本手册后面将对此进行描述。
例子列表
demo.cpp:
这是本教程中使用的完整示例。它的作用如下:
- 创建不同类型的站点、路线和时间表的结构
- 显示他
- 用一条语句将它序列化到一个名为“testfile.txt”的文件
- 还原为另一种结构
- 显示恢复的结构
这个程序的输出足以验证这个系统满足了序列化系统的所有最初陈述的要求。存档文件的内容也可以显示为序列化文件的ASCII文本。
demo_xml.cpp
这是原始演示的一个变体,除了支持其他演示外,还支持xml存档。需要使用额外的包装宏BOOST_SERIALIZATION_NVP(name)将数据项名称与相应的xml标记关联起来。重要的是“name”是一个有效的xml标记,否则将不可能恢复存档。有关更多信息,请参见名称-值对。下面是xml归档文件的样子。
demo_xml_save.cpp 和demo_xml_load.cpp
还请注意,虽然我们的示例将程序数据保存并加载到同一个程序中的存档中,但这仅仅是为了便于说明。通常,归档文件可能由创建它的同一程序加载,也可能不加载。
敏锐的读者可能会注意到,这些例子包含了一个微妙但重要的缺陷。内存泄漏。在main函数中创建总线站点。公共汽车时刻表可以多次参考这些公共汽车站点。在主要功能结束后,对公交时刻表进行销毁,对公交车站进行销毁。这似乎很好。但是如何处理从归档文件加载过程中创建的结构new_schedule数据项呢?它包含自己单独的一组公共汽车站点,这些站点在公共汽车时刻表之外不被引用。这些不会在程序的任何地方被破坏——内存泄漏。
有几种方法可以解决这个问题。一种方法是显式管理公交车站。然而,更健壮和透明的方法是使用shared_ptr而不是原始指针。除了标准库的序列化实现,序列化库还包括boost::shared ptr的序列化实现。鉴于此,应该很容易修改这些示例中的任何一个以消除内存泄漏。这留给读者作为练习。