C++ Boost

序列化

教程


一个非常简单的案例
非侵入式版本
可序列化成员
派生类
指针
数组
STL 集合
类版本控制
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 函数,该函数保存 latitudelongitude。然后,每个成员都将通过在 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 将授予序列化库访问私有成员变量和函数的权限。

指针

假设我们将公交路线定义为公交站的数组。鉴于
  1. 我们可能有几种类型的公交站(记住 bus_stop 是一个基类)
  2. 给定的 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(){}
};

数组 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(){}
};

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。当加载存档时,将读取它保存时的版本号。上面的代码可以修改为利用这一点


#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() 生成代码,该代码根据归档是用于保存还是加载来调用 saveload

归档

我们在这里的讨论重点是向类添加序列化功能。序列化数据的实际呈现是在归档类中实现的。因此,序列化数据的流是类序列化和所选归档的产物。这些两个组件独立是关键设计决策。这允许任何序列化规范可用于任何归档。

在本教程中,我们使用了一个特定的归档类 - text_oarchive 用于保存,text_iarchive 用于加载。文本归档以文本形式呈现数据,并且可在跨平台移植。除了文本归档外,该库还包括用于本地二进制数据和 xml 格式数据的归档类。所有归档类的接口都相同。一旦为某个类定义了序列化,该类就可以序列化到任何类型的归档中。

如果当前的归档类集不提供特定应用程序所需的属性、格式或行为,则可以创建一个新的归档类或从现有类派生。这将在手册的后面部分进行说明。

示例列表

demo.cpp
这是本教程中使用的完整示例。它执行以下操作
  1. 创建一个包含不同类型停止、路线和计划的结构
  2. 显示它
  3. 使用一条语句将其序列化到名为“testfile.txt”的文件中
  4. 恢复到另一个结构
  5. 显示恢复的结构
该程序的输出 足以验证此系统满足序列化系统的所有最初要求。 归档文件的内容 也可以显示为序列化文件,因为它们是 ASCII 文本。
demo_xml.cpp
这是原始演示的一个变体,除了其他演示外,它还支持 xml 归档。需要额外的包装宏 BOOST_SERIALIZATION_NVP(name) 将数据项名称与相应的 xml 标签关联。重要的是“name”必须是一个有效的 xml 标签,否则将无法恢复归档。有关更多信息,请参阅 名称-值对这里 是一个 xml 归档的样子。
demo_xml_save.cppdemo_xml_load.cpp
还要注意,虽然我们的示例将程序数据保存和加载到同一个程序中的存档中,但这仅仅是为了说明方便。通常,归档可能由创建它的程序加载,也可能不加载。

细心的读者可能会注意到这些示例中存在一个微妙但重要的缺陷:它们存在内存泄漏。公交站是在 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)