C++ Boost

序列化

可序列化概念


基本类型
类类型
成员函数
自由函数
自由函数重载的命名空间
类成员
基类
const 成员
模板
版本控制
serialize 拆分为 save/load
成员函数
自由函数
指针
非默认构造函数
派生类对象的指针
注册
导出
实例化
选择性跟踪
运行时类型转换
引用
数组
类序列化特征
序列化包装器
模型 - 库中包含的序列化实现
当且仅当满足以下条件之一时,类型 T 才是**可序列化**的

基本类型

上述存档类的模板运算符 &、<< 和 >> 将生成代码,以将所有基本类型保存/加载到/从存档中。此代码通常只是根据存档格式将数据添加到存档中。例如,一个四字节整数作为 4 个字节附加到二进制存档中,而对于文本存档,它将呈现为空格后跟字符串表示形式。

类类型

对于类/结构体类型,模板运算符 &、<< 和 >> 将生成调用程序员为特定数据类型编写的序列化代码的代码。没有默认值。尝试序列化未明确指定序列化的类/结构体将导致编译时错误。可以通过类成员函数或将对该类实例的引用作为参数的自由函数来指定类的序列化。

成员函数

序列化库调用以下代码以将类实例保存/加载到/从存档中。

template<class Archive, class T>
inline void serialize(
    Archive & ar, 
    T & t, 
    const unsigned int file_version
){
    // invoke member function for class T
    t.serialize(ar, file_version);
}
也就是说,模板 serialize 的默认定义假定存在以下签名的类成员函数模板

template<class Archive>
void serialize(Archive &ar, const unsigned int version){
    ...
}
如果未声明此类成员函数,则会发生编译时错误。为了能够调用此模板生成的成员函数将数据附加到存档,它必须是公共的,或者必须通过在类定义中包含以下内容来使序列化库可以访问该类

friend class boost::serialization::access;
应首选后一种方法,而不是将成员函数设为公共的选项。这将防止从库外部调用序列化函数。这几乎可以肯定是一个错误。不幸的是,它可能看起来可以正常工作,但以一种很难找到的方式失败。

对于同一个模板如何同时用于将数据保存到存档和从存档加载数据,可能不会立即明白。关键在于,对于输出存档,& 运算符定义为 <<,而对于输入存档,则定义为 >>& 的“多态”行为允许同一个模板用于保存和加载操作。这非常方便,因为它节省了大量输入,并保证类数据成员的保存和加载始终同步。这是整个序列化系统的关键。

自由函数

当然,我们不限于使用上述默认实现。我们可以用我们自己的实现来覆盖默认实现。这样做将允许我们在不更改类定义本身的情况下实现类的序列化。我们称之为**非侵入式**序列化。假设我们的类名为 my_class,则重写将指定为

// namespace selection

template<class Archive>
inline void serialize(
    Archive & ar, 
    my_class & t, 
    const unsigned int file_version
){
    ...
}
请注意,我们将此重写称为“非侵入式”。这稍微有点不准确。它并不要求类具有特殊函数,也不要求它从某个公共基类派生或进行任何其他基本设计更改。但是,它将需要访问要保存和加载的类成员。如果这些成员是 private 的,则无法序列化它们。因此,在某些情况下,即使使用这种“非侵入式”方法,也需要对要序列化的类进行细微修改。在实践中,这可能不是什么问题,因为许多库(例如 STL)公开了足够的信息,允许实现非侵入式序列化,而无需对库进行任何更改。

自由函数重载的命名空间

为了获得最大程度的可移植性,请在命名空间 boost::serialization 中包含任何自由函数模板和定义。如果可移植性不是问题,并且使用的编译器支持 ADL(参数依赖查找),则自由函数和模板可以在以下任何命名空间中

请注意,乍一看,对于实现两阶段查找的编译器来说,此建议似乎是错误的。事实上,序列化库使用了一种可能过于聪明的方法来支持此规则,即使对于此类编译器也是如此。有兴趣进一步研究此问题的读者可以在 serialization.hpp 中找到更多信息

类成员的序列化

无论使用上述哪种方法,serialize 函数的主体都必须通过将存档 operator & 依次应用于类的所有数据成员来指定要保存/加载的数据。

{
    // save/load class member variables
    ar & member1;
    ar & member2;
}

基类

头文件 base_object.hpp 包含模板

template<class Base, class Derived>
Base & base_object(Derived &d);
该模板应用于创建对基类对象的引用,该引用可用作存档序列化运算符的参数。因此,对于**可序列化**类型 T 的类,基类状态应像这样序列化

{
    // invoke serialization of the base class 
    ar & boost::serialization::base_object<base_class_of_T>(*this);
    // save/load class member variables
    ar & member1;
    ar & member2;
}
抵制住将 *this 强制转换为基类的诱惑。这似乎可行,但可能无法调用正确序列化所需的代码。

请注意,这与调用基类的 serialize 函数**不同**。这似乎可行,但会绕过某些用于跟踪对象、注册基类-派生类关系以及序列化系统按设计运行所需的其他簿记的代码。因此,所有 serialize 成员函数都应为 private

const 成员

const 成员保存到存档不需要特殊考虑。可以使用 const_cast 来解决加载 const 成员的问题

    ar & const_cast<T &>(t);
请注意,这违反了 const 关键字的精神和意图。const 成员在构造类实例时初始化,之后不会更改。但是,在许多情况下,这可能是最合适的。最终,这取决于在序列化上下文中 const 的含义是什么。

模板

模板的序列化实现与普通类的过程完全相同,不需要额外的考虑。除其他外,这意味着如果定义了组件模板的序列化,则在需要时会自动生成模板组合的序列化。例如,该库包含 boost::shared_ptr<T>std::list<T> 的序列化定义。如果我已经为自己的类 my_t 定义了序列化,那么 std::list< boost::shared_ptr< my_t> > 的序列化已经可以使用了。

有关如何为自己的类模板实现此想法的示例,请参阅 demo_auto_ptr.cpp。这显示了如何为标准库中的模板 auto_ptr 实现非侵入式序列化。

在示例 shared_ptr.hpp 中可以找到将序列化添加到标准模板中稍微棘手一些的示例

在模板的序列化规范中,通常将 serialize 拆分为 load/save 对。请注意,上文中描述的便捷宏在这些情况下没有帮助,因为模板类参数的数量和类型与为简单类拆分 serialize 时使用的参数不匹配。请改用重写语法。

版本控制

最终会出现这种情况,即在创建存档后更改类定义。保存类实例时,当前版本包含在存储在存档中的类信息中。从存档加载类实例时,原始版本号将作为参数传递给加载函数。这允许加载函数包含逻辑来适应类的旧定义,并使它们与最新版本保持一致。保存函数始终保存当前版本。因此,这会导致自动将旧格式的存档转换为最新版本。版本号针对每个类独立维护。这导致了一个简单的系统,允许访问旧文件并转换它们。类的当前版本被分配为类序列化特征,本手册稍后将对此进行介绍。

{
    // invoke serialization of the base class 
    ar & boost::serialization::base_object<base_class_of_T>(*this);
    // save/load class member variables
    ar & member1;
    ar & member2;
    // if its a recent version of the class
    if(1 < file_version)
        // save load recently added class members
        ar & member3;
}

serialize 拆分为 Save/Load

有时,对保存和加载函数使用相同的模板会很不方便。例如,如果版本控制变得复杂,就可能会发生这种情况。

拆分成员函数

对于成员函数,可以通过包含头文件 boost/serialization/split_member.hpp 来解决此问题,并在类中包含如下代码

template<class Archive>
void save(Archive & ar, const unsigned int version) const
{
    // invoke serialization of the base class 
    ar << boost::serialization::base_object<const base_class_of_T>(*this);
    ar << member1;
    ar << member2;
    ar << member3;
}

template<class Archive>
void load(Archive & ar, const unsigned int version)
{
    // invoke serialization of the base class 
    ar >> boost::serialization::base_object<base_class_of_T>(*this);
    ar >> member1;
    ar >> member2;
    if(version > 0)
        ar >> member3;
}

template<class Archive>
void serialize(
    Archive & ar,
    const unsigned int file_version 
){
    boost::serialization::split_member(ar, *this, file_version);
}
这将序列化拆分为两个独立的函数 saveload。由于新的 serialize 模板始终相同,因此可以通过调用头文件 boost/serialization/split_member.hpp 中定义的宏 BOOST_SERIALIZATION_SPLIT_MEMBER() 来生成它。因此,上面的整个 serialize 函数可以用以下内容替换

BOOST_SERIALIZATION_SPLIT_MEMBER()

拆分自由函数

对于使用免费的 serialize 函数模板进行的非侵入式序列化,情况也是如此。 要使用 saveload 函数模板而不是 serialize
 namespace boost { namespace serialization { template<class Archive> void save(Archive & ar, const my_class & t, unsigned int version) { ... } template<class Archive> void load(Archive & ar, my_class & t, unsigned int version) { ... } }} 
包含头文件
boost/serialization/split_free.hpp 并重写免费的 serialize 函数模板

namespace boost { namespace serialization {
template<class Archive>
inline void serialize(
    Archive & ar,
    my_class & t,
    const unsigned int file_version
){
    split_free(ar, t, file_version); 
}
}}
为了缩短输入,上述模板可以用宏来代替

BOOST_SERIALIZATION_SPLIT_FREE(my_class)
请注意,尽管提供了将 serialize 函数拆分为 save/load 的功能,但最好使用带有相应 & 运算符的 serialize 函数。序列化实现的关键在于对象的保存和加载顺序完全相同。使用 & 运算符和 serialize 函数可以确保始终如此,并将最大限度减少与 saveload 函数同步相关的难以发现的错误。

还要注意,BOOST_SERIALIZATION_SPLIT_FREE 必须在任何命名空间之外使用。

指针

可以使用任何存档保存/加载运算符序列化指向任何类实例的指针。

要通过指针正确保存和恢复对象,必须解决以下情况

  1. 如果通过不同的指针多次保存同一个对象,则只需保存该对象的一个副本。
  2. 如果通过不同的指针多次加载一个对象,则应该只创建一个新对象,并且所有返回的指针都应该指向它。
  3. 系统必须检测到首先通过指针保存对象,然后保存对象本身的情况。如果不采取额外的预防措施,加载将导致创建原始对象的多个副本。本系统在保存时会检测到这种情况并抛出异常 - 参见下文。
  4. 派生类的对象可以通过指向基类的指针来存储。必须确定并保存对象的真实类型。在恢复时,必须创建正确的类型,并将其地址正确转换为基类。也就是说,必须考虑多态指针。
  5. 保存时必须检测到 NULL 指针,并在反序列化时恢复为 NULL。
此序列化库解决了上述所有注意事项。通过指针保存和加载对象的过程并不简单。它可以概括如下

保存指针

  1. 确定指向的对象的真实类型。
  2. 向存档写入特殊标记
  3. 如果指向的对象尚未写入存档,请立即执行此操作
加载指针
  1. 从存档中读取标记。
  2. 确定要创建的对象的类型
  3. 如果对象已加载,则返回其地址。
  4. 否则,创建对象的新实例
  5. 使用上述运算符读回数据
  6. 返回新创建对象的地址。
假设类实例只保存/加载到存档一次,无论使用 <<>> 运算符序列化多少次通过指向基类的指针序列化派生类型的指针可能需要一些额外的“帮助”。此外,程序员可能希望出于自身原因修改上述过程。例如,可能希望抑制对对象的跟踪,因为先验地知道,所讨论的应用程序永远不会创建重复的对象。指针的序列化可以通过指定类序列化特征 来“微调”,如 本手册的另一部分 中所述

非默认构造函数

指针的序列化在库中使用类似于以下代码实现

// load data required for construction and invoke constructor in place
template<class Archive, class T>
inline void load_construct_data(
    Archive & ar, T * t, const unsigned int file_version
){
    // default just uses the default constructor to initialize
    // previously allocated memory. 
    ::new(t)T();
}
默认的 load_construct_data 调用默认构造函数“就地”初始化内存。

如果没有这样的默认构造函数,则必须重写函数模板 load_construct_data,可能还要重写 save_construct_data。下面是一个简单的例子


class my_class {
private:
    friend class boost::serialization::access;
    const int m_attribute;  // some immutable aspect of the instance
    int m_state;            // mutable state of this instance
    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version){
        ar & m_state;
    }
public:
    // no default construct guarantees that no invalid object
    // ever exists
    my_class(int attribute) :
        m_attribute(attribute),
        m_state(0)
    {}
};
重写将是

namespace boost { namespace serialization {
template<class Archive>
inline void save_construct_data(
    Archive & ar, const my_class * t, const unsigned int file_version
){
    // save data required to construct instance
    ar << t->m_attribute;
}

template<class Archive>
inline void load_construct_data(
    Archive & ar, my_class * t, const unsigned int file_version
){
    // retrieve data from archive required to construct new instance
    int attribute;
    ar >> attribute;
    // invoke inplace constructor to initialize instance of my_class
    ::new(t)my_class(attribute);
}
}} // namespace ...
除了指针的反序列化之外,这些重写还用于其元素类型没有默认构造函数的 STL 容器的反序列化。

派生类对象的指针

注册

请考虑以下内容

class base {
    ...
};
class derived_one : public base {
    ...
};
class derived_two : public base {
    ...
};
int main(){
    ...
    base *b;
    ...
    ar & b; 
}
保存 b 时应该保存哪种对象?加载 b 时应该创建哪种对象?它应该是类 derived_onederived_two 还是 base 的对象?

事实证明,序列化的对象类型取决于基类(在本例中为 base)是否是多态的。如果 base 不是多态的,也就是说,如果它没有虚函数,则将序列化类型 base 的对象。任何派生类中的信息都将丢失。如果这就是期望的结果(通常不是),则无需其他操作。

如果基类是多态的,则将序列化最派生类型(在本例中为 derived_onederived_two)的对象。要序列化哪种类型的对象的问题(几乎)由库自动处理。

系统在第一次序列化该类的对象时,会在存档中“注册”每个类,并为其分配一个序列号。下次在同一个存档中序列化该类的对象时,该数字将写入存档中。因此,每个类在存档中都被唯一标识。当读回存档时,每个新的序列号都会与正在读取的类重新关联。请注意,这意味着在保存和加载期间都必须进行“注册”,以便在加载时构建的类整数表与在保存时构建的类整数表相同。事实上,整个序列化系统的关键是事物总是以相同的顺序保存和加载。这包括“注册”。

扩展我们之前的例子


int main(){
    derived_one d1;
    derived_two d2:
    ...
    ar & d1;
    ar & d2;
    // A side effect of serialization of objects d1 and d2 is that
    // the classes derived_one and derived_two become known to the archive.
    // So subsequent serialization of those classes by base pointer works
    // without any special considerations.
    base *b;
    ...
    ar & b; 
}
当读取 b 时,它前面有一个唯一的(对于存档)类标识符,该标识符先前已与类 derived_onederived_two 相关联。

如果派生类尚未按上述方式自动“注册”,则在调用序列化时将抛出 unregistered_class 异常。

这可以通过显式注册派生类来解决。所有存档都派生自一个基类,该基类实现以下模板


template<class T>
register_type(T * = NULL);
所以我们的问题也可以通过编写以下代码来解决

int main(){
    ...
    ar.template register_type<derived_one>();
    ar.template register_type<derived_two>();
    base *b;
    ...
    ar & b; 
}
请注意,如果序列化函数在保存和加载之间拆分,则两个函数都必须包含注册。这是为了保持保存和相应的加载同步所必需的。

导出

以上方法有效,但可能不方便。当我们编写代码以通过基类指针进行序列化时,我们并不总是知道要序列化哪些派生类。每次编写新的派生类时,我们都必须返回到序列化基类的所有地方并更新代码。

所以我们还有另一种方法


#include <boost/serialization/export.hpp>
...
BOOST_CLASS_EXPORT_GUID(derived_one, "derived_one")
BOOST_CLASS_EXPORT_GUID(derived_two, "derived_two")

int main(){
    ...
    base *b;
    ...
    ar & b; 
}
BOOST_CLASS_EXPORT_GUID 将字符串字面量与类相关联。在上面的例子中,我们使用了类名的字符串呈现。如果通过指针序列化此类“导出”类的对象,并且该对象未注册,则“导出”字符串将包含在存档中。稍后读取存档时,将使用字符串字面量来查找应该由序列化库创建的类。这允许每个类与它的字符串标识符一起放在一个单独的头文件中。不需要维护可能被序列化的派生类的单独“预注册”。这种注册方法称为“密钥导出”。有关此主题的更多信息,请参见“类特征 - 导出密钥”一节。

实例化

通过上述任何方法进行注册都实现了另一个重要作用,其重要性可能并不明显。此系统依赖于 template<class Archive, class T> 形式的模板化函数。这意味着必须为程序中序列化的每个存档和数据类型组合实例化序列化代码。

程序可能永远不会显式引用派生类的多态指针,因此通常永远不会实例化序列化此类类的代码。因此,除了在存档中包含导出密钥字符串之外,BOOST_CLASS_EXPORT_GUID 还为程序使用的所有存档类显式实例化类序列化代码。

选择性跟踪

是否跟踪对象取决于其 对象跟踪特征。用户定义类型的默认设置是 track_selectively。也就是说,当且仅当对象通过程序中任何位置的指针序列化时才跟踪它们。通过上述任何方式“注册”的任何对象都被假定为通过程序中某个位置的指针序列化,因此将被跟踪。在某些情况下,这可能会导致效率低下。假设我们有一个供多个程序使用的类模块。因为某些程序序列化了指向此类对象的指针,所以我们在类头文件中指定 BOOST_CLASS_EXPORT导出 类标识符。当另一个程序包含此模块时,将始终跟踪此类的对象,即使可能不需要这样做。这种情况可以通过在这些程序中使用 track_never 来解决。

也可能发生这样的情况:即使程序通过指针进行序列化,我们更关心的是效率,而不是避免创建重复对象的可能性。可能是我们碰巧知道不会有重复项。也可能是创建一些重复项是良性的,考虑到跟踪重复项的运行时成本,不值得避免。同样,可以使用 track_never

运行时类型转换

为了在运行时正确地在基类指针和派生类指针之间进行转换,系统要求每个基类/派生类对都可以在一个表中找到。使用 boost::serialization::base_object<Base>(Derived &) 序列化基类对象的一个副作用是,确保在进入 main 函数之前将基类/派生类对添加到表中。这非常方便,并且可以使语法更加简洁。唯一的问题是,当通过指针序列化派生类,而不需要调用其基类序列化时,就会发生这种情况。在这种情况下,有两种选择。显而易见的一种是使用 base_object 调用基类序列化,并为基类序列化指定一个空函数。另一种方法是通过调用模板 void_cast_register<Derived, Base>(); 来显式“注册”基类/派生类关系。请注意,这里使用的术语“注册”与其在前一节中的用法无关。下面是如何做到这一点的示例

#include <sstream>
#include <boost/serialization/serialization.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/export.hpp>

class base {
    friend class boost::serialization::access;
    //...
    // only required when using method 1 below
    // no real serialization required - specify a vestigial one
    template<class Archive>
    void serialize(Archive & ar, const unsigned int file_version){}
};

class derived : public base {
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int file_version){
        // method 1 : invoke base class serialization
        ar & boost::serialization::base_object<base>(*this);
        // method 2 : explicitly register base/derived relationship
        boost::serialization::void_cast_register<derived, base>(
            static_cast<derived *>(NULL),
            static_cast<base *>(NULL)
        )
    }
};

BOOST_CLASS_EXPORT_GUID(derived, "derived")

int main(){
    //...
    std::stringstream ss;
    boost::archive::text_iarchive ar(ss);
    base *b;
    ar >> b; 
}

为了使该模板能够在由不符合标准的编译器编译的代码中被调用,可以使用以下语法


boost::serialization::void_cast_register(
    static_cast<Derived *>(NULL),
    static_cast<Base *>(NULL)
);
有关详细信息,请参阅 模板调用语法

引用

包含引用成员的类通常需要非默认构造函数,因为引用只能在构造实例时设置。如果类具有引用成员,则上一节的示例会稍微复杂一些。这就引出了被引用的对象如何存储、存储在哪里以及如何创建的问题。此外,还有一个关于对多态基类的引用的问题。基本上,这些问题与指针相同。这并不奇怪,因为引用实际上是一种特殊的指针。我们通过将引用序列化为指针来解决这些问题。

class object;
class my_class {
private:
    friend class boost::serialization::access;
    int member1;
    object & member2;
    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version);
public:
    my_class(int m, object & o) :
        member1(m), 
        member2(o)
    {}
};
重写将是

namespace boost { namespace serialization {
template<class Archive>
inline void save_construct_data(
    Archive & ar, const my_class * t, const unsigned int file_version
){
    // save data required to construct instance
    ar << t.member1;
    // serialize reference to object as a pointer
    ar << & t.member2;
}

template<class Archive>
inline void load_construct_data(
    Archive & ar, my_class * t, const unsigned int file_version
){
    // retrieve data from archive required to construct new instance
    int m;
    ar >> m;
    // create and load data through pointer to object
    // tracking handles issues of duplicates.
    object * optr;
    ar >> optr;
    // invoke inplace constructor to initialize instance of my_class
    ::new(t)my_class(m, *optr);
}
}} // namespace ...

数组

如果 T 是可序列化类型,则任何类型为 T 的原生 C++ 数组都是可序列化类型。也就是说,如果 T 是可序列化类型,则以下内容将自动可用并按预期工作

T t[4];
ar << t;
    ...
ar >> t;

类序列化特征

序列化包装器

模型 - 库中包含的序列化实现

上述功能足以实现所有 STL 容器的序列化。事实上,这已经完成,并已包含在库中。例如,为了对 std::list 使用包含的序列化代码,请使用

#include <boost/serialization/list.hpp>
而不是

#include <list>
由于前者包含后者,因此只需这样做即可。这同样适用于所有 STL 集合以及支持它们所需的模板(例如 std::pair)。

截至撰写本文时,该库包含以下 boost 类的序列化

C++17 的 std::variant 也受支持。其他类正在添加到列表中,因此请查看 boost 文件部分和标题以获取新的实现!

© Copyright Robert Ramey 2002-2004。在 Boost 软件许可证 1.0 版下分发。(请参阅随附文件 LICENSE_1_0.txt 或访问 https://boost.ac.cn/LICENSE_1_0.txt 获取副本)