C++ Boost

序列化

特殊说明


对象跟踪
类信息
辅助函数支持
归档可移植性
数值
特征
二进制归档
XML 归档
导出类序列化
静态库和序列化
DLL - 序列化和运行时链接
插件
多线程
优化
归档异常
异常安全性

对象跟踪

取决于类如何使用以及其他因素,序列化后的对象可能会按内存地址进行跟踪。这可防止同一个对象多次写入或读出归档。这些存储的地址还可用于删除在因抛出异常而中断了的加载过程中创建的对象。

如果在程序中将不同对象的副本从同一个地址保存,那么可能会导致问题。


template<class Archive>
void save(boost::basic_oarchive & ar, const unsigned int version) const
{
    for(int i = 0; i < 10; ++i){
        A x = a[i];
        ar << x;
    }
}
在这种情况下,要保存的数据存在于堆栈中。循环的每次迭代都会更新堆栈上的值。因此,虽然数据每次迭代都发生变化,但数据的地址却没有变化。如果 a[i] 是由内存地址跟踪的对象数组,库将跳过存储第一个之后的的对象,因为它假定同一地址上的对象实际上是同一个对象。

为了帮助检测这些情况,输出归档运算符希望能传递 const 引用参数。

这样,上面代码会触发一个编译时断言。此示例中一个明显的修复方法是使用


template<class Archive>
void save(boost::basic_oarchive & ar, const unsigned int version) const
{
    for(int i = 0; i < 10; ++i){
        ar << a[i];
    }
}
该方法将编译并运行,没有问题。输出归档运算符使用 const,将确保序列化的进程不会改变序列化对象的的状态。尝试这样做会造成状态保存的概念增强为实际上不明显的副作用。这几乎肯定会是一个错误,并可能导致非常微妙的 bug。

不幸的是,当前实现问题阻止了在数据项封装为名称-值对时检测这类错误。

当不同的对象加载到与最终位置不同的地址时,也可能发生类似问题


template<class Archive>
void load(boost::basic_oarchive & ar, const unsigned int version) const
{
    for(int i = 0; i < 10; ++i){
        A x;
        ar >> x;
        std::m_set.insert(x);
    }
}
在这种情况下,跟踪的是 x 的地址,而不是添加到集合的新项的地址。如果不解决,这将破坏依赖于跟踪的功能,比如通过指针加载对象。程序中将引入极不明显的缺陷。可以通过如下方式更改以上代码来解决此问题

template<class Archive>
void load(boost::basic_iarchive & ar, const unsigned int version) const
{
    for(int i = 0; i < 10; ++i){
        A x;
        ar >> x;
        std::pair<std::set::const_iterator, bool> result;
        result = std::m_set.insert(x);
        ar.reset_object_address(& (*result.first), &x);
    }
}
这将调整跟踪信息以反映移动变量的最终放置位置,从而纠正以上问题。

如果事先已知没有指针值被重复,那么通过适当设置对象跟踪类序列化特性,可以消除与对象跟踪相关的开销。

默认情况下,实现级别类序列化特性指定的原始数据类型永远不会被跟踪。如果希望通过指针跟踪共享的原始对象(例如,用作引用计数的long),应该将其包装在类/结构中,使其成为可标识的类型。更改long的实现级别的替代方法会影响程序中序列化的所有long——这可能不是预期结果。

即使对象永远不会通过指针序列化,我们也可能希望跟踪地址。例如,虚拟基类只需要保存/加载一次。通过将此序列化特性设置为track_always,我们可以阻止冗余保存/加载操作。


BOOST_CLASS_TRACKING(my_virtual_base_class, boost::serialization::track_always)

辅助函数支持

某些类型,特别是那些生命周期行为复杂或对其内部状态访问受限的类型,可能需要或受益于精心设计的序列化算法。激励此原则的案例是shared_ptr。实例加载后,必须将其与已经加载的任何其他实例“匹配”。因此,在加载包含shared_ptr实例的归档文件时,必须保留已加载实例的表。如果不维护此表,shared_ptr将成为可序列化类型。

为实现此功能,可以声明与当前归档文件关联的帮助器对象,该对象可用于存储与特定类型序列化算法相关的上下文信息。


template<class T>
class shared_ptr
{
   ...
};

BOOST_SERIALIZATION_SPLIT_FREE(shared_ptr)

class shared_ptr_serialization_helper
{
  // table of previously loaded shared_ptr
  // lookup a shared_ptr from the object address
  shared_ptr<T> lookup(const T *);
  // insert a new shared_ptr
  void insert<shared_ptr<T> >(const shared_ptr<T> *);
};

namespace boost {
namespace serialization {

template<class Archive>
void save(Archive & ar, const shared_ptr & x, const unsigned int /* version */)
{
    // save shared ptr
    ...
}

template<class Archive>
void load(Archive & ar, shared_ptr & x, const unsigned int /* version */)
{
    // get a unique identifier.  Using a constant means that all shared pointers
    // are held in the same set.  Thus we detect handle multiple pointers to the
    // same value instances in the archive.
    const void * shared_ptr_helper_id = 0;

    shared_ptr_serialization_helper & hlp =
        ar.template get_helper<shared_ptr_serialization_helper>(helper_instance_id);

    // load shared pointer object
    ...

    shared_ptr_serialization_helper & hlp =
        ar.template get_helper<shared_ptr_serialization_helper>(shared_ptr_helper_id);

    // look up object in helper object
    T * shared_object hlp.lookup(...);

    // if found, return the one from the table

    // load the shared_ptr data
    shared_ptr<T> sp = ...

    // and add it to the table
    hlp.insert(sp);
    // implement shared_ptr_serialization_helper load algorithm with the aid of hlp
}

} // namespace serialization
} // namespace boost
get_helper<shared_ptr_serialization_helper>(); 首次调用时会为与归档文件关联的帮助器对象创建一个帮助器对象;后续调用会返回在第一个调用中创建的对象的引用,以便 hlp 可以有效地用于存储通过同一归档文件对不同 complex_type对象序列化时持续存在的上下文信息。

可以创建帮助器来保存和加载归档文件。同一程序可能有多个不同的帮助器,或将同一帮助器从程序的不同部分单独实例化。这就是 helper_instance_id 必需的原因。原则上它可以是任意的唯一整数。实际上,使用包含它的序列化函数的地址似乎最容易。上面的示例使用了这种技术。

类信息

默认情况下,对于每个序列化类,都会将类信息写入存档中。此信息包括版本号、实现级别和跟踪行为。这是有必要的,以便即使程序の後期版本更改类的一些当前特征值,也可正确反序列化存档。此数据的空间开销是最小的。由于必须检查每个类来查看是否已將其类信息包含在存档中,因此运行时开销稍微大一些。在某些情况下,即使这可能被认为太多。可以通过将 实现级别 类特征设置为来消除此额外开销。 boost::serialization::object_serializable.

关闭跟踪和类信息序列化将生成纯模板内联代码,原则上可以优化为简单的流写/读。以这种方式消除所有序列化开销会付出代价。将存档发布给用户后,无法更改类序列化特征,否则会使旧存档无效。将类信息包括在存档中可以确保即使类定义经过修改,这些存档将来仍然可读。像显示像素这样的轻量级结构可以在标头中声明,如下所示:


#include <boost/serialization/serialization.hpp>
#include <boost/serialization/level.hpp>
#include <boost/serialization/tracking.hpp>

// a pixel is a light weight struct which is used in great numbers.
struct pixel
{
    unsigned char red, green, blue;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int /* version */){
        ar << red << green << blue;
    }
};

// elminate serialization overhead at the cost of
// never being able to increase the version.
BOOST_CLASS_IMPLEMENTATION(pixel, boost::serialization::object_serializable);

// eliminate object tracking (even if serialized through a pointer)
// at the risk of a programming error creating duplicate objects.
BOOST_CLASS_TRACKING(pixel, boost::serialization::track_never)

归档可移植性

多个存档类以文本或可移植的二进制格式创建数据。应该可以在一个平台上保存这样的类,并在另一个平台上加载它。这取决于几个条件。

数值

读取存档的机器的架构必须能够保存已保存的数据。例如,gcc 编译器保留 4 个字节来存储类型为 wchar_t 的变量,而其他编译器只保留 2 个字节。因此,有可能编写无法由加载程序表示的值。这是一个相当明显的情况,通过使用 <boost/cstdint.hpp> 中的数字类型很容易处理。

一个特殊的整数类型是 std::size_t,它是一个整数类型的类型定义,保证足够大以容纳任何集合的大小,但其实际大小可能因平台而异。collection_size_type 封装器存在是为了让存档以可移植的方式序列化集合大小。可移植地序列化集合大小的推荐选将是使用 64 位或可变长度整数表示。

特征

以下示例说明了另一个潜在问题

template<class T>
struct my_wrapper {
    template<class Archive>
    Archive & serialize ...
};

...

class my_class {
    wchar_t a;
    short unsigned b;
    template<class Archive>
    Archive & serialize(Archive & ar, unsigned int version){
        ar & my_wrapper(a);
        ar & my_wrapper(b);
    }
};
如果 my_wrapper 使用默认序列化特征,可能会出现问题。使用默认特征时,每次将新类型添加到归档文件时,都会添加簿记信息。因此,在此示例中,该归档文件将包含 my_wrapper<wchar_t>my_wrapper<short_unsigned> 的簿记信息。否则呢?对于将 wchar_t 视为 unsigned short 同义词的编译器又如何呢?在这种情况下,只有一个不同类型,而不是两个。如果归档文件在处理 wchar_t 时差异的编译器之间传递程序,则加载操作将以灾难性的方式失败。

一种补救方法是将序列化特征分配给模板 my_template,以便此模板的实例化类的信息永远不会被序列化。此过程已在上文中描述,并已用于名称-值对。此类特征通常将分配给包装器。

避免此问题的另一种方法是向模板 my_wrapper 的所有专门化(针对所有基本类型)分配序列化特征,以便永远不会保存类信息。这是我们对 STL 集合序列化的实现所做的工作。

二进制归档

某些系统上的标准流 i/o 会在输出时将换行符扩展为回车/换行符。这会给二进制归档文件带来问题。处理此问题的最简单方法是使用标志 ios::binary 以“二进制模式”打开二进制归档文件的流。如果不这样做,则生成的文件将无法读取。

遗憾的是,没有找到在加载归档文件之前检测此错误的方法。当检测到此错误时,调试版本会断言,因此这可能有助于发现此错误。

XML 归档

XML 归档文件表现为一个有点特殊的情况。XML 格式具有一个嵌套结构,它非常适合序列化系统所使用的“递归类成员访客”模式。不过,XML 与其他格式的不同之处在于,它需要为每个数据成员指定一个名称。我们的目标是将此信息添加到类序列化规范中,同时仍允许在任何归档文件中使用序列化代码。这可以通过要求将序列化到 XML 归档文件中的所有数据都序列化为 名称-值对 来实现。第一个成员用作数据项的 XML 标签,第二个成员引用数据项本身。尝试序列化时,如果数据未包装在 名称-值对 中,将在编译时导致错误。这样的实现方式适用于其他归档类,序列化时仅使用了数据的 value 部分。name 部分在编译期间会舍弃。因此,通过始终使用 名称-值对,可以确保以最有效的形式将所有数据序列化到所有归档类中。

导出类序列化

在本手册中的其他地方,我们已经介绍了 BOOST_CLASS_EXPORT。Export 意味着两件事在 C++ 中,使用未被明确引用的代码是通过虚函数来实现的。因此,通过基类指针或引用来操作派生类的使用方式隐含了对 Export 的需求。

在包含任何归档类头文件的源模块中,BOOST_CLASS_EXPORT 将实例化代码,以将指定类型的多态指针序列化到所有这些归档类中。如果未包含任何归档类头文件,那么将不会实例化任何代码。

请注意,若要实现此功能,则 BOOST_CLASS_EXPORT 宏必须在包含要为其实例化代码的归档类头文件之后出现。因此,使用 BOOST_CLASS_EXPORT 的代码将如下所示


#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
... // other archives

#include "a.hpp" // header declaration for class a
BOOST_CLASS_EXPORT(a)
... // other class headers and exports
无论代码是独立的可执行文件、静态库还是动态/共享库的一部分,上述内容都是正确的。

如果将 BOOST_CLASS_EXPORT放在头文件 “a.hpp” 本身中,就像对其他序列化特征所做的那样,那么将很难遵循上面关于在调用 BOOST_CLASS_EXPORT 之前包含归档头文件的规则,甚至不可能遵循该规则。最好的解决办法是在头文件声明中使用 BOOST_CLASS_EXPORT_KEY,而在类定义文件中使用 BOOST_CLASS_EXPORT_IMPLEMENT

此系统对将代码放置在静态或共享库中会产生一定的影响。如果将 BOOST_CLASS_EXPORT 放置在库代码中,除非总体类头也被包括在内,否则它不会产生任何作用。因此,在构建库时,应当为打算使用的所有总体类指定所有头。或者,可仅包含 多态总体 的头。

严格地说,如果通过最派生类的指针进行所有指针串行化,就不需要导出。但是,为了检测到什么会成为灾难性错误,库通过指针捕获所有串行化,而该指针指向未导出或以其他方式注册的多态类。所以,在实践中,请准备注册或导出通过指针串行化的一个或多个虚拟函数的所有类。

需要注意,此功能的实现依赖于 C++ 语言的特定供应商扩展。因此,不保证使用此功能的程序具有可移植性。然而,所有已使用 boost 测试的 C++ 编译器都提供了所需的扩展。该库包含这些编译器中每一个所需的额外声明。有理由预期,未来的 C++ 编译器将支持这些扩展或与之等同的东西。

静态库和序列化

数据类型的串行化代码可以保存在库中,就像可以保存其他类型的实现一样。这会运行得很好,并且可以节省大量编译时间。通过 demo_pimpl.cpp demo_pimpl_A.cpp demo_pimpl_A.hpp 说明这一点,串行化的实现在一个独立于主程序的静态库中。

DLL - 序列化和运行时链接

序列化代码可以置于运行时链接的库中。也就是说,代码可以置于 DLLS(Windows) 共享库(*nix) 或静态库以及主可执行文件中。最佳技术与上述对库的描述相同。序列化库测试套件包括下列程序来说明这种工作方式

test_dll_simple dll_a.cpp ,其中序列化的实现也与主程序完全分开,但代码在运行时加载。在此示例中,该代码在使用它的程序启动时自动加载,但也可以使用与操作系统相关的 API 调用加载和卸载。

还包括 test_dll_exported.cpp polymorphic_derived2.cpp ,与上述类似,但包括在 DLLS 语境下对 export 和 no_rtti 函数进行的测试。

为了获得最佳效果,请编写符合以下指导原则的代码

插件

为了实现库,需要使用各种在运行时操控类型的函数。它们是 extended_type_info,用于将类与外部标识字符串(GUID)相关联,以及 void_cast,用于在相关类型的指针之间进行类型转换。为了完成 extended_type_info 的功能,已添加构建和销毁对应类型的功能。为了使用此功能,必须指定如何创建每种类型。应在导出类时执行此操作。因此,上述代码的更完整的示例如下

#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
... // other archives

#include "a.hpp" // header declaration for class a

// this class has a default constructor
BOOST_SERIALIZATION_FACTORY_0(a)
// as well as one that takes one integer argument
BOOST_SERIALIZATION_FACTORY_1(a, int)

// specify the GUID for this class
BOOST_CLASS_EXPORT(a)
... // other class headers and exports
将此就位后,可以构建、序列化和销毁一个只知道其GUID和基类的类。

多线程

序列化的基本目的与同时从/向单个开放归档实例写入/读取多个线程冲突。库实现假定应用程序避免了这种情况。

但是,在不同的任务中同时写入/读取不同的归档是允许的,因为每个归档实例几乎完全独立于其他归档实例。唯一共享的信息是一些使用无锁线程安全 单例 实现的类型表,在本文档的其他位置描述过。

此单例实现保证在包含它的代码模块被加载时初始化所有这些共享信息。序列化库负责确保这些数据结构不会随后被修改。唯一可能出现问题的时间是代码在另一个任务序列化数据时加载/卸载。这只能发生对于其序列化在动态加载/卸载 DLL 或共享库中实现的类型。因此,如果避免下列情况

该库应为线程安全。

优化

在序列化大量连续同类数据的高性能应用中,我们希望避免单独序列化每个元素的开销,这是array包装器的动机。包含连续同类数组的数据类型的序列化函数,比如std::vectorstd::valarrayboost::multiarray,应当使用array包装器来序列化它们,以利用这些优化。可以为连续同类数组提供优化的序列化的归档类型应当通过重载array包装器的序列化来实现,就像针对二进制归档所做的那样。

归档异常

异常安全性


© 版权所有 罗伯特·拉梅 2002-2004。在 Boost 软件许可证第 1.0 版下发布。(参见随文件提供的 LICENSE_1_0.txt 或 https://boost.ac.cn/LICENSE_1_0.txt 上的副本)