|
序列化特殊说明 |
如果在程序中将不同对象的副本从同一个地址保存,那么可能会导致问题。
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)
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 集合序列化的实现所做的工作。
ios::binary
以“二进制模式”打开二进制归档文件的流。如果不这样做,则生成的文件将无法读取。遗憾的是,没有找到在加载归档文件之前检测此错误的方法。当检测到此错误时,调试版本会断言,因此这可能有助于发现此错误。
BOOST_CLASS_EXPORT
。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++ 编译器将支持这些扩展或与之等同的东西。
BOOST_CLASS_EXPORT_KEY
。BOOST_CLASS_EXPORT_IMPLEMENT
。对于任何特定类型,应当只有一个包含该类型 BOOST_CLASS_EXPORT_IMPLEMENT
的文件。这可以确保程序中只存在串行化代码的一个副本。避免浪费空间,避免在同一程序中拥有不同版本的串行化代码。在多个文件中包含 BOOST_CLASS_EXPORT_IMPLEMENT
,可能会导致由于重复符号或抛出运行时异常而链接失败。demo_pimpl.cpp
、 demo_pimpl_A.cpp
和 demo_pimpl_A.hpp
说明这一点,串行化的实现在一个独立于主程序的静态库中。
test_dll_simple
和 dll_a.cpp
,其中序列化的实现也与主程序完全分开,但代码在运行时加载。在此示例中,该代码在使用它的程序启动时自动加载,但也可以使用与操作系统相关的 API 调用加载和卸载。
还包括 test_dll_exported.cpp
和 polymorphic_derived2.cpp
,与上述类似,但包括在 DLLS 语境下对 export 和 no_rtti 函数进行的测试。
为了获得最佳效果,请编写符合以下指导原则的代码
inline
代码。这将在 DLLS 和主干中生成重复的代码。这会不必要地重复代码。更糟糕的是,它使得不同版本的同一段代码可以同时存在。这种类型的错误极其难以调试。最后,它使得被引用的模块有可能被明确卸载,这(希望)会导致运行时出错。这是另一个不容易重现或查找的 bug。对于类成员模板,在头文件中使用类似
template<class Archive>
void serialize(Archive & ar, const unsigned int version);
而在实现文件中使用
template<class Archive>
void myclass::serialize(Archive & ar, const unsigned int version){
...
}
BOOST_CLASS_EXPORT_IMPLEMENT(my_class)
#include <boost/archive/text_oarchive>
#include <boost/archive/text_iarchive>
template myclass::serialize(boost::archive::text_oarchive & ar, const unsigned int version);
template myclass::serialize(boost::archive::text_iarchive & ar, const unsigned int version);
... // repeat for each archive class to be used.
这将在仅一处生成所有必需的代码。库不会检测您自己的这种类型的错误。dlopen
或在 Windows 中使用 LoadLibrary
)。尝试按相反的顺序卸载它们。即使未遵循上述指南,这也应确保避免问题。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::vector
、std::valarray
或boost::multiarray
,应当使用array
包装器来序列化它们,以利用这些优化。可以为连续同类数组提供优化的序列化的归档类型应当通过重载array
包装器的序列化来实现,就像针对二进制归档所做的那样。© 版权所有 罗伯特·拉梅 2002-2004。在 Boost 软件许可证第 1.0 版下发布。(参见随文件提供的 LICENSE_1_0.txt 或 https://boost.ac.cn/LICENSE_1_0.txt 上的副本)