一个类型T是可序列化的,当且仅当以下条件之一为真:
- 它是一个基本类型。
通过基本类型,我们指的是C++内置类型,并且仅指C++内置类型。算术类型(包括字符)、bool、枚举都是基本类型。在下面的序列化特性中,我们为了不同的目的以不同的方式定义了“基本”实现级别。这可能会引起混淆。 - 它是一个类类型,并且已经按照下面详细描述的原型之一进行了声明:
a. 类成员函数serialize
b. 全局函数serialize - 它是一个指向可序列化类型的指针。
- 它是一个指向可序列化类型的引用。
- 它是可序列化类型的本地C++数组。
1.基本类型(Primitive Types)
上述归档类的模板操作符&、<<和>>将生成代码以将所有基本类型保存/加载到归档中。这段代码通常会根据归档格式将数据添加到归档中。例如,一个四个字节的整数作为一个二进制归档被附加为4个二进制字节,而对于文本归档,它将被呈现为一个空格后跟一个字符串表示。
2.类类型(Class Types)
对于类/结构类型,模板运算符&、<<和>>将生成调用程序员为特定数据类型编写的序列化代码的代码。没有默认值。尝试对未明确指定序列化的类/结构进行序列化将导致编译时错误。可以通过类成员函数或接受类实例引用作为参数的自由函数来指定类的序列化。
2.1成员函数(Member Function)
序列化库调用的以下代码可以将类实例保存到或从档案中加载。
template<class Archive, class T>
inline void serialize(
Archive & ar,
T & t,
const unsigned int file_version
){
// 调用类T的成员函数
t.serialize(ar, file_version);
}
也就是说,默认定义的模板函数 serialize 假定存在以下签名的类成员函数模板:
template<class Archive>
void serialize(Archive &ar, const unsigned int version){
...
}
如果没有声明这样的成员函数,将发生编译时错误。为了使该模板生成的成员函数能够被调用以将数据追加到存档中,它必须是public的,或者必须通过在类定义中包含以下内容来使类对序列化库可访问:
friend class boost::serialization::access;
应该优先选择后一种方法,而不是将成员函数设为public。这将防止从库外部调用序列化函数。这几乎肯定是一个错误。不幸的是,它可能看起来能够正常工作,但是可能以非常难以找到的方式失败。
可能不会立即明显这个模板如何同时用于将数据保存到存档中和从存档中加载数据。关键在于&运算符被定义为输出存档的<<和输入存档的>>。&的“多态”行为使得相同的模板可以用于保存和加载操作。这非常方便,因为它节省了很多输入,并保证了类数据成员的保存和加载始终保持同步。这是整个序列化系统的关键所在。
2.2 Free Function
当然,我们不受限于上述描述的默认实现。我们可以使用自己的覆盖默认实现。这样做将允许我们在不修改类定义本身的情况下实现类的序列化。我们称之为非侵入式序列化。假设我们的类名为my_class,覆盖的实现可以如下指定:
// namespace selection
template<class Archive>
inline void serialize(
Archive & ar,
my_class & t,
const unsigned int file_version
){
...
}
请注意,我们称这种覆盖为“非侵入式”。这有点不准确。它不要求类具有特殊的函数,也不要求它从某个共同的基类派生,也没有其他基本的设计改变。然而,它将需要访问要保存和加载的类成员。**如果这些成员是私有的,就无法对它们进行序列化。**因此,在使用这种“非侵入式”方法时,有时候会需要对要序列化的类进行一些微小的修改。在实际应用中,这可能并不是什么问题,因为许多库(例如STL)提供了足够的信息,可以完全不修改库的情况下实现非侵入式序列化。
2.2.1 Namespaces for Free Function Overrides
为了最大限度地提高可移植性,将任何自由函数模板和定义包含在命名空间boost::serialization中。如果可移植性不是问题,并且使用的编译器支持ADL(Argument Dependent Lookup)(参数依赖查找),则自由函数和模板可以在以下任何命名空间中:
- boost::serialization
- archive类的命名空间
- 被序列化的类型的命名空间
需要注意的是,对于实现了两阶段查找的编译器,这个建议可能会看起来不合适。然而,实际上,序列化库采用了一种巧妙的方法来支持这一规则,即使对于这样的编译器也适用。如果您对此问题进一步研究感兴趣,可以在 serialization.hpp 中找到更多信息。
2.3 类成员的序列化(Serialization of Class Members)
无论使用以上的哪种方法,serialize函数的主体都必须通过顺序应用存档运算符&来指定要保存/加载的数据成员。
{
// 保存/加载类成员变量
ar & member1;
ar & member2;
}
2.3.1 基类(Base Classes)
头文件base_object.hpp包括以下模板:
template<class Base, class Derived>
Base & base_object(Derived &d);
应该用于创建一个基类对象的引用,该引用可以作为参数传递给存档序列化运算符。因此,对于Serializable类型T的类,基类状态应该像这样进行序列化:
{
// 调用基类的序列化
ar & boost::serialization::base_object<base_class_of_T>(*this);
// 保存/加载类成员变量
ar & member1;
ar & member2;
}
不要轻易将*this强制转换为基类。这可能看起来可行,但可能无法调用必要的代码以实现正确的序列化。
请注意,这与调用基类的serialize函数不同。这样做可能看起来可行,但会绕过用于跟踪对象、注册基类派生关系和其他必要的序列化系统设计的簿记代码。因此,所有serialize成员函数应该是私有的。
2.3.2 const成员
将const成员保存到存档不需要特别考虑。加载const成员可以通过使用const_cast 来处理:
ar & const_cast<T &>(t);
请注意,这违反了const关键字的精神和意图。const成员在类实例构造时初始化,之后不会更改。然而,在许多情况下,这可能是最合适的方法。最终,这涉及到在序列化上下文中const意味着什么的问题。
2.3.3Templates
-
模板的实现序列化与普通类完全相同,不需要额外考虑。这意味着,如果已定义了组件模板的序列化,那么在需要时,模板组合的序列化会自动生成。例如,此库包含了对boost::shared_ptr 和std::list 的序列化定义。如果我已为我的自定义类my_t定义了序列化,那么std::list< boost::shared_ptr< my_t> > 的序列化已经可以供使用。
-
要查看如何为自己的类模板实现这个想法的示例,请参阅demo_auto_ptr.cpp。这个示例展示了如何实现标准库中的auto_ptr模板的非侵入式序列化。
-
有时,向标准模板添加序列化会有些棘手,可以在shared_ptr.hpp示例中找到。
-
在模板的序列化规范中,通常将serialize分为加载(load)和保存(save)两部分。请注意,上面描述的方便宏在这些情况下不起作用,因为模板类参数的数量和类型不会与将serialize拆分为简单类时使用的参数匹配。请改用覆盖(override)语法。
2.4 Versioning
在创建存档之后,类定义最终会发生变化。当保存类实例时,当前版本将包括在存档中存储的类信息中。当从存档中加载类实例时,原始版本号将作为加载函数的参数传递。这使得加载函数能够包含逻辑来适应类的旧定义并将其与最新版本协调一致。保存函数始终保存当前版本。因此,这会自动将旧格式的存档转换为最新版本。每个类都会独立维护版本号。这样就实现了一个简单的系统,允许访问旧文件并将其转换为最新版本。类的当前版本将被分配为稍后在本手册中描述的"Class Serialization Trait"(类序列化特性)。
{
// 调用基类的序列化
ar & boost::serialization::base_object<base_class_of_T>(*this);
// 保存/加载类成员变量
ar & member1;
ar & member2;
// 如果是类的最近版本
if (1 < file_version)
// 保存/加载最近添加的类成员
ar & member3;
}
2.5 Splitting serialize into Save/Load
将serialize拆分为保存(save)和加载(load)。有时,对于保存和加载函数,使用相同的模板可能会不方便,例如,如果版本控制变得复杂时。
拆分成员函数
对于成员函数,可以通过在类中包含头文件boost/serialization/split_member.hpp,并在类中添加以下代码来处理:
template<class Archive>
void save(Archive & ar, const unsigned int version) const
{
// 调用基类的序列化
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)
{
// 调用基类的序列化
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);
}
这将将序列化拆分为两个独立的函数:save和load。由于新的serialize模板始终相同,可以通过在头文件boost/serialization/split_member.hpp中定义的宏BOOST_SERIALIZATION_SPLIT_MEMBER()来生成它。因此,上面的整个serialize函数可以替换为:
BOOST_SERIALIZATION_SPLIT_MEMBER()
拆分自由函数
对于非侵入式序列化的自由序列化函数模板,情况与上述类似。如果要使用保存和加载函数模板而不是serialize:
在命名空间boost::serialization中,包含头文件boost/serialization/split_free.hpp,并覆盖自由序列化函数模板:
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(my_class)
请注意,尽管提供了将serialize函数拆分为save/load函数的功能,但首选使用serialize函数和相应的 & 运算符。序列化实现的关键是对象以完全相同的顺序保存和加载。使用 & 运算符和serialize函数可以确保这种情况始终成立,并将最小化与保存和加载函数同步的难以查找的错误的发生。
此外,请注意,BOOST_SERIALIZATION_SPLIT_FREE必须在任何命名空间之外使用。
3. Pointers
任何类实例的指针都可以使用存档(archive)的保存/加载操作符进行序列化。要正确保存和还原通过指针操作对象,需要处理以下情况:
-
如果通过不同的指针多次保存同一对象,只需要保存一个对象的副本。
-
如果通过不同的指针多次加载同一对象,只需创建一个新对象,并且所有返回的指针应该指向它。
-
系统必须检测到对象首先通过指针保存,然后再保存对象本身的情况。如果没有采取额外的预防措施,加载将导致创建多个原始对象的副本。在保存时,该系统会检测到这种情况并抛出异常 - 请参见下文。
-
派生类的对象可以通过基类的指针进行存储。必须确定对象的真实类型并保存它。在恢复时,必须正确创建正确类型的对象,并将其地址正确转换为基类。也就是说,必须考虑多态指针。
-
当保存时必须检测NULL指针,并在反序列化时将其还原为NULL。
这个序列化库解决了上述所有问题。通过指针保存和加载对象的过程是复杂的,可以总结如下:
保存指针:
- 确定指向的对象的真实类型。
- 在存档中写入一个特殊的标记。
- 如果指向的对象尚未写入存档中,现在将其写入。
加载指针:
- 从存档中读取一个标记。
- 确定要创建的对象类型。
- 如果对象已经加载,返回其地址。
- 否则,创建对象的新实例。
- 使用上述描述的操作符读取数据。
- 返回新创建对象的地址。
由于类实例只会被保存/加载一次,不管它们被多次使用<<和>>操作符序列化,多次加载相同的指针对象只会创建一个对象,因此会复制原始指针配置。
对于包含多态指针的结构,无需额外的用户努力即可处理。
通过基类的指针对派生类型的指针进行序列化可能需要一些额外的“帮助”。此外,程序员可能出于自己的原因希望修改上述过程。例如,可能希望抑制对象跟踪,因为已经预先知道应用程序永远不会创建重复的对象。通过指定类序列化特性,可以通过本手册的另一部分进行指针序列化的“微调”。
3.1 非默认构造函数(Non-Default Constructors)
指针的序列化是通过类似以下代码在库中实现的:
// 加载用于构造并在适当位置调用构造函数的数据
template<class Archive, class T>
inline void load_construct_data(
Archive & ar, T * t, const unsigned int file_version
){
// 默认情况下,使用默认构造函数在先前分配的内存中初始化
::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; // 实例的某些不可变属性
int m_state; // 该实例的可变状态
template<class Archive>
void serialize(Archive &ar, const unsigned int file_version){
ar & m_state;
}
public:
// 没有默认构造函数,以确保不会存在无效对象
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
){
// 保存构造实例所需的数据
ar << t->m_attribute;
}
template<class Archive>
inline void load_construct_data(
Archive & ar, my_class * t, const unsigned int file_version
){
// 从存档中检索构造新实例所需的数据
int attribute;
ar >> attribute;
// 调用就地构造函数以初始化 my_class 实例
::new(t)my_class(attribute);
}
}} // namespace ...
除了指针的反序列化,这些覆盖还用于反序列化STL容器,其元素类型没有默认构造函数的情况。
3.2 指向派生类对象的指针(Pointers to Objects of Derived Classes)
3.3.1 注册(Registration)
考虑以下情况:
class base {
// ...
};
class derived_one : public base {
// ...
};
class derived_two : public base {
// ...
};
main(){
// ...
base *b;
// ...
ar & b;
}
在保存b时,应该保存哪种类型的对象?在加载b时,应该创建哪种类型的对象?是derived_one类、derived_two类,还是base类的对象?
事实证明,序列化的对象类型取决于基类(在这种情况下是base)是否具有多态性。如果base不是多态的,也就是说它没有虚函数,那么将序列化base类型的对象。派生类中的任何信息都将丢失。如果这是所期望的(通常不是),则不需要其他努力。
如果基类具有多态性,那么将序列化最派生类型的对象(在这种情况下是derived_one或derived_two)。序列化哪种类型的对象的问题(几乎)由库自动处理。
系统在归档中首次序列化该类对象时为每个类进行“注册”,并为其分配一个顺序号。下次在同一归档中序列化该类对象时,此号码将写入归档。因此,每个类在归档内部都有唯一的标识。在读取归档时,每个新的序列号都将与正在读取的类重新关联。请注意,这意味着“注册”必须在保存和加载期间都发生,以便在加载期间建立的类-整数表与保存期间建立的类-整数表相同。实际上,整个序列化系统的关键在于事物总是以相同的顺序保存和加载。这包括“注册”。
扩展我们之前的示例:
main(){
derived_one d1;
derived_two d2:
// ...
ar & d1;
ar & d2;
// 对象d1和d2的序列化的副作用是使派生类derived_one和derived_two成为归档的已知类。
// 因此,后续通过基类指针对这些类进行的序列化无需任何特殊考虑即可工作。
base *b;
// ...
ar & b;
}
在读取b时,它之前会带有一个唯一的(对于该归档)类标识符,该标识符已与derived_one类或derived_two类关联。
如果派生类没有像上述自动“注册”,那么在调用序列化时将抛出unregistered_class异常。
可以通过显式注册派生类来解决此问题。所有归档都是从实现以下模板的基类派生的:
template<class T>
register_type();
因此,我们的问题也可以通过以下方式解决:
main(){
// ...
ar.template register_type<derived_one>();
ar.template register_type<derived_two>();
base *b;
// ...
ar & b;
}
请注意,如果序列化函数在保存和加载之间分割,那么两个函数都必须包括注册。这是为了保持保存和相应的加载的同步。
3.3.2 Export
上述方法可以工作,但可能不太方便。当我们编写通过基类指针序列化派生类对象的代码时,我们并不总是知道要序列化哪些派生类。每当写入新的派生类时,我们都必须返回到序列化基类的所有位置并更新代码。
因此,我们有另一种方法:
#include <boost/serialization/export.hpp>
...
BOOST_CLASS_EXPORT_GUID(derived_one, "derived_one")
BOOST_CLASS_EXPORT_GUID(derived_two, "derived_two")
main(){
// ...
base *b;
// ...
ar & b;
}
宏BOOST_CLASS_EXPORT_GUID将字符串字面量与类关联起来。在上面的示例中,我们使用了类名的字符串表示。如果通过指针序列化这种“导出”的类的对象,并且此类对象未注册,那么该“导出”字符串将包含在存档中。稍后在读取存档时,将使用字符串字面量来查找应由序列化库创建的类。这允许每个类都可以在单独的头文件中与其字符串标识符一起。无需维护可能需要序列化的派生类的单独“预注册”。这种注册方法称为“关键导出”。有关此主题的更多信息可以在“类特性 - 导出关键”部分找到。
3.3.3实例化(Instantiation)
通过上述任何一种方法进行的注册还具有一个可能不太明显但重要的作用。该系统依赖于形式为template<class Archive, class T>的模板函数。这意味着必须为程序中序列化的每种存档和数据类型的组合实例化序列化代码。
通常,程序永远不会显式引用派生类的多态指针,因此通常不会实例化序列化这类类的代码。因此,除了在归档中包含导出关键字符串之外,BOOST_CLASS_EXPORT_GUID还显式实例化了程序使用的所有存档类的类序列化代码。
3.3.4选择性跟踪
对象是否被跟踪是由其对象跟踪特性决定的。用户定义类型的默认设置是track_selectively,即仅在程序的任何地方通过指针序列化它们时才跟踪对象。通过上述任何一种方式“注册”的任何对象都被假定为在程序的某个地方通过指针序列化,并因此将被跟踪。在某些情况下,这可能会导致效率低下。假设我们有一个被多个程序使用的类模块。由于某些程序序列化了该类的多态指针对象,我们在类头文件中指定了BOOST_CLASS_EXPORT以导出类标识符。当此模块被另一个程序包含时,该类的对象将始终被跟踪,即使可能并不需要跟踪。可以通过在这些程序中使用track_never来解决这种情况。
还可能出现这种情况,即使程序通过指针序列化,我们更关心效率而不是避免创建重复对象的可能性。可能我们碰巧知道不会有重复。还可能创建一些重复对象在运行时成本方面不值得避免。同样,可以使用track_never。
3.4引用
包含引用成员的类通常需要非默认构造函数,因为引用只能在实例构造时设置。如果类具有引用成员,前一节的示例会稍微复杂一些。这引发了一个关于被引用对象如何存储和在哪里创建的问题。还有关于对多态基类的引用的问题。基本上,这些问题与指针相关的问题是相同的。这并不奇怪,因为引用实际上是一种特殊类型的指针。我们通过将引用序列化为指针来解决这些问题。
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
){
// 保存构造实例所需的数据
ar << t.member1;
// 将引用序列化为指针
ar << & t.member2;
}
template<class Archive>
inline void load_construct_data(
Archive & ar, my_class * t, const unsigned int file_version
){
// 从存档中检索构造新实例所需的数据
int m;
ar >> m;
// 创建并通过指向对象的指针加载数据
// 跟踪处理重复问题。
object * optr;
ar >> optr;
// 调用就地构造函数以初始化 my_class 实例
::new(t)my_class(m, *optr);
}
}} // namespace ...
数组
如果T是可序列化类型,则任何T类型的本机C++数组也是可序列化类型。也就是说,如果T是可序列化类型,以下内容将自动可用并将按预期运行:
T t[4];
ar << t;
...
ar >> t;
https://www.boost.org/doc/libs/1_51_0/libs/serialization/doc/index.html