|
序列化教程 |
serialize
拆分为 save/load
ar << data;
ar & data;
输入归档类似于输入数据流。数据可以使用 >> 或 & 运算符从归档中加载。
ar >> data;
ar & data;
当对基本数据类型调用这些运算符时,数据只是简单地保存/加载到/从归档中。当对类数据类型调用时,类 serialize
函数被调用。每个 serialize
函数使用上述运算符来保存/加载其数据成员。此过程将以递归方式继续,直到保存/加载类中包含的所有数据。
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");
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
类的成员。两种公式的功能完全相同。
非侵入式序列化的主要应用是允许对类实现序列化,而无需更改类定义。为了使这成为可能,该类必须公开足够的信息来重建类状态。在这个例子中,我们假设该类有 public
成员 - 这并不常见。只有公开足够信息来保存和恢复类状态的类才能在不更改类定义的情况下进行序列化。
一个具有可序列化成员的可序列化类如下所示
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
类的实例将调用 serialize
函数,该函数保存 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_)
{}
};
注意从派生类中序列化基类。不要直接调用基类序列化函数。这样做似乎可以工作,但会绕过跟踪写入存储的实例以消除冗余的代码。它还会绕过将类版本信息写入归档的操作。出于这个原因,建议始终将成员 serialize
函数设为私有。声明 friend boost::serialization::access
将授予序列化库访问私有成员变量和函数的权限。
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(){}
};
数组 stops
的每个成员都将被序列化。但请记住每个成员都是一个指针 - 所以这到底意味着什么?这个序列化的全部目的是允许在另一个地方和时间重建原始数据结构。为了用指针实现这一点,保存指针的值是不够的,而必须保存它指向的对象。当成员稍后被加载时,必须创建一个新对象,并且必须将一个新指针加载到类成员中。如果同一个指针被序列化多次,则只会在归档中添加一个实例。读回时,不会读回任何数据。唯一执行的操作是将第二个指针设置为等于第一个
请注意,在这个示例中,数组包含多态指针。也就是说,每个数组元素都指向几种可能的公交站之一。因此,当保存指针时,必须保存某种类标识符。当加载指针时,必须读取类标识符,并必须构造相应的类的实例。最后,数据可以加载到新创建的正确类型实例中。正如在 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(){}
};
#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。当加载存档时,将读取它保存时的版本号。上面的代码可以修改为利用这一点
#include <boost/serialization/list.hpp>
#include <boost/serialization/string.hpp>
#include <boost/serialization/version.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)
{
// 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)
通过将版本控制应用于每个类,无需尝试维护文件的版本控制。也就是说,文件版本是其所有组成类的版本的组合。此系统允许程序始终与由程序的所有先前版本创建的存档兼容,而无需比此示例所需的更多工作。serialize
拆分为 save/load
serialize
函数简单、简洁,并保证类成员以相同的顺序保存和加载 - 这是序列化系统的关键。但是,在某些情况下,加载和保存操作并不像这里使用的示例那样相似。例如,这可能发生在一个经历过多个版本的类中。上面的类可以重新公式化为
#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
。在本教程中,我们使用了一个特定的归档类 - text_oarchive
用于保存,text_iarchive
用于加载。文本归档以文本形式呈现数据,并且可在跨平台移植。除了文本归档外,该库还包括用于本地二进制数据和 xml 格式数据的归档类。所有归档类的接口都相同。一旦为某个类定义了序列化,该类就可以序列化到任何类型的归档中。
如果当前的归档类集不提供特定应用程序所需的属性、格式或行为,则可以创建一个新的归档类或从现有类派生。这将在手册的后面部分进行说明。
细心的读者可能会注意到这些示例中存在一个微妙但重要的缺陷:它们存在内存泄漏。公交站是在 main
函数中创建的。公交时刻表可能多次引用这些公交站。在公交时刻表销毁后,主函数结束时,公交站也被销毁。这看起来没问题。但是,由从存档加载过程创建的结构 new_schedule
数据项又如何呢?它包含自己独立的公交站集,这些公交站集在公交时刻表之外没有被引用。这些公交站不会在程序中的任何地方被销毁 - 这就是内存泄漏。
有两种方法可以解决这个问题。一种方法是显式管理公交站。但是,一种更健壮且透明的方法是使用 shared_ptr
而不是原始指针。除了标准库的序列化实现之外,序列化库还包含 boost::shared ptr
的序列化实现。有了这些,修改任何这些示例以消除内存泄漏应该很容易。这留作读者的练习。
© 版权所有 Robert Ramey 2002-2004。根据 Boost 软件许可证版本 1.0 分发。 (参见附带的 LICENSE_1_0.txt 文件或复制到 https://boost.ac.cn/LICENSE_1_0.txt)