C++ 序列化实用指南
理解 Boost 序列化的最佳方式是通过逐步复杂化的序列化场景进行演示。
简而言之,序列化就是将数据和对象写入一个载体(文件、缓冲区、套接字),以便之后在同一台或另一台计算主机的内存中进行重构。这个重构过程也称为反序列化。
序列化布尔值、整数或浮点数等基本类型非常简单:只需按原样写入数据(假设没有使用压缩)。序列化指针则不同:它指向的对象必须先被序列化。这样,反序列化指针就仅仅是将它的值设置为对象重构后所在的内存地址。
根据指针(和引用)图的复杂程度,我们可以区分序列化的三个复杂级别:
- 指针图是一个*森林*(即一组*树*)。数据可以简单地按自下而上、深度优先遍历树的方式进行序列化。
- 指针图是一个*有向无环图*(DAG),即一个没有环的图。我们仍然可以按自下而上的方式序列化数据,确保共享数据只写入和恢复一次。
- 指针图是一个一般图,即它可能包含环。我们需要写入和恢复带有前向引用的数据,以便正确处理环。
使用自定义代码序列化对象始终是一个选项。然而,序列化比简单的美化打印方法要复杂得多。人们希望序列化支持以下功能:
- 序列化应能处理任何指针图(即带环)。
- 序列化指针或引用应自动触发对所引用对象的序列化。
- 序列化整个数据模型可能需要大量的代码——从简单的标量字段(布尔值、整数、浮点数),到容器(vector、list、hash table 等),再到复杂的数据结构(图、四叉树、稀疏矩阵等)。人们希望模板能承担大部分工作。
- 保存和加载函数必须始终保持同步:如果修改了“保存”函数,“加载”函数也必须相应地更改。人们希望这个过程尽可能自动化。
- 应该有一种方法可以在不更改类*.hpp*文件的情况下序列化对象——这称为非侵入式序列化。原因是,在许多情况下,人们不希望(或无法)更改现有库的源文件。
- 序列化需要支持版本控制。随着对象的演变,数据成员会被添加或删除,并且期望能向后兼容——也就是说,仍然可以将旧版本存档中的数据反序列化到最新数据模型中。
- 序列化应跨平台兼容(32 位和 64 位机器、Windows、Linux、Solaris 等)。
Boost 库提供的序列化功能满足以上所有要求,并且更多。
- 它效率极高,支持版本控制,并且会自动序列化 STL 容器。
- 序列化(保存函数)和反序列化(加载函数)通过一个模板来实现,这减少了代码量并解决了同步问题。
- 在一些帮助下,Boost 序列化还支持 32 位和 64 位兼容,这意味着在 32 位机器上序列化的数据库可以在 64 位机器上读取,反之亦然。
- 此外,Boost 序列化(或反序列化)接受的输出(或输入)参数与 `std::ostream`(或 `std::istream`)非常相似,这意味着它可以是磁盘上的文件、缓冲区或套接字。您可以直接通过网络序列化您的数据。
理解 Boost 序列化的最佳方式是通过逐步复杂化的序列化场景进行演示。
基本序列化
序列化代码以及保存和恢复简单对象的示例代码如下。
#pragma once
// File obj.hpp
// Forward declaration of class boost::serialization::access
namespace boost {
namespace serialization {
class access;
}
}
class Obj {
public:
// Serialization expects the object to have a default constructor
Obj() : d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : d1_(d1), d2_(d2) {}
bool operator==(const Obj& o) const {
return d1_ == o.d1_ && d2_ == o.d2_;
}
private:
int d1_;
bool d2_;
// Allow serialization to access non-public data members.
friend class boost::serialization::access;
template<typename Archive>
void serialize(Archive& ar, const unsigned version) {
ar & d1_ & d2_; // Simply serialize the data members of Obj
}
};
模板 `serialize` 定义了保存和加载。这是通过将 `&` 运算符定义为输出(或输入)存档的 `<<`(或 `>>`)来实现的。请注意 `friend` 声明,它允许保存/加载模板访问对象的私有数据成员。另外请注意,序列化要求对象具有默认构造函数(该构造函数可以是私有的)。
#include "obj.hpp"
#include <assert.h>
#include <fstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
int main() {
const char* fileName = "saved.txt";
// Create some objects
const Obj o1(-2, false);
const Obj o2;
const Obj o3(21, true);
const Obj* const p1 = &o1;
// Save data
{
// Create an output archive
std::ofstream ofs(fileName);
boost::archive::text_oarchive ar(ofs);
// Write data
ar & o1 & o2 & o3 & p1;
}
// Restore data
Obj restored_o1;
Obj restored_o2;
Obj restored_o3;
Obj* restored_p1;
{
// Create and input archive
std::ifstream ifs(fileName);
boost::archive::text_iarchive ar(ifs);
// Load data
ar & restored_o1 & restored_o2 & restored_o3 & restored_p1;
}
// Make sure we restored the data exactly as it was saved
assert(restored_o1 == o1);
assert(restored_o2 == o2);
assert(restored_o3 == o3);
assert(restored_p1 != p1);
assert(restored_p1 == &restored_o1);
return 0;
}
在 `main.cpp` 中,我们首先包含声明输入和输出文本存档的头文件,对象将从中加载和保存。我们创建一个输出存档(这里是磁盘上的文件),并写入 `Obj` 类的三个实例,以及其中一个实例的指针。然后我们读取它们,并确保数据恢复原样。请注意恢复的指针 `restored_p1` 如何指向恢复的对象 `restored_o1`。
指针序列化详解
每当我们对指针(或引用)进行序列化操作时,只要有必要,就会触发对它指向(或引用)的对象进行序列化。因此,我们无需显式序列化被指向的对象,因为 Boost 序列化会自动确保指针图中到达的对象被序列化。
例如,下面的代码表明序列化指针 `p1` 会触发对它指向的对象 `o1` 的序列化。在恢复指针 `restored_p1` 时,我们会自动创建一个对象 `o1` 的克隆。
#include "obj.hpp"
#include <assert.h>
#include <fstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
int main()
{
const char* fileName = "saved.txt";
// Create one object o1.
const Obj o1(-2, false);
const Obj* const p1 = &o1;
// Save data
{
// Create an output archive
std::ofstream ofs(fileName);
boost::archive::text_oarchive ar(ofs);
// Save only the pointer. This will trigger serialization
// of the object it points too, i.e., o1.
ar & p1;
}
// Restore data
Obj* restored_p1;
{
// Create and input archive
std::ifstream ifs(fileName);
boost::archive::text_iarchive ar(ifs);
// Load
ar & restored_p1;
}
// Make sure we read exactly what we saved.
assert(restored_p1 != p1);
assert(*restored_p1 == o1);
return 0;
}
反序列化指针时,如果它指向的对象尚未被反序列化,则该对象将被自动反序列化。这意味着不应在反序列化指向该对象的指针*之后*尝试反序列化该对象。原因是,一旦指针反序列化强制了对象反序列化,您就无法在不同的地址重新构建该对象。
#include "obj.hpp"
#include <fstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
int main()
{
const char* fileName = "saved.txt";
std::ofstream ofs(fileName);
// Create one object o1 and a pointer p1 to that object.
const Obj o1(-2, false);
const Obj* const p1 = &o1;
// Serialize object, then pointer.
// This works fine: after the object is deserialized, we can
// deserialize the pointer by assigning it to the object’s address.
{
boost::archive::text_oarchive ar(ofs);
ar & o1 & p1;
}
// Serialize pointer, then object.
// This does not work: once p1 has been serialized, the object
// has already been deserialized and its address cannot change.
// This will throw an instance of 'boost::archive::archive_exception'
// at runtime.
{
boost::archive::text_oarchive ar(ofs);
ar & p1 & o1;
}
return 0;
}
在上面的示例中,第二次序列化将导致运行时错误。
ocoudert@MyMacBookPro $ a.out
terminate called after throwing an instance of 'boost::archive::archive_exception'
what(): pointer conflict
Abort trap
coudert@MyMacBookPro $
这意味着,当需要序列化指针时,我们永远不应显式序列化它们指向的对象。
显式保存和加载函数定义
当保存和加载函数不完全对称时,我们需要显式定义它们。这通常发生在涉及版本控制的情况下。请注意 `BOOST_SERIALIZATION_SPLIT_MEMBER()` 宏的使用,它负责在使用输出/输入存档时调用保存/加载。
#pragma once
#include <boost/serialization/split_member.hpp>
class Obj {
public:
Obj() : d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : d1_(d1), d2_(d2) {}
bool operator==(const Obj& o) const {
return d1_ == o.d1_ && d2_ == o.d2_;
}
private:
int d1_;
bool d2_;
friend class boost::serialization::access;
template<class Archive>
void save(Archive & ar, const unsigned int version) const {
ar & d1_ & d2_;
}
template<class Archive>
void load(Archive & ar, const unsigned int version) {
ar & d1_ & d2_;
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
};
C 字符串的序列化
C 字符串不能直接序列化,因为它假定 `char*` 的特定解释,即一个以空字符('\0')结尾的字符数组。因此,我们需要显式序列化 C 字符串。下面的类是一个简单的 C 字符串序列化辅助类(请注意,这可以通过避免构造 `std::string` 来优化)。
#pragma once
// File SerializeCStringHelper.hpp
#include <string>
#include <boost/serialization/string.hpp>
#include <boost/serialization/split_member.hpp>
class SerializeCStringHelper {
public:
SerializeCStringHelper(char*& s) : s_(s) {}
SerializeCStringHelper(const char*& s) : s_(const_cast<char*&>(s)) {}
private:
friend class boost::serialization::access;
template<class Archive>
void save(Archive& ar, const unsigned version) const {
bool isNull = (s_ == 0);
ar & isNull;
if (!isNull) {
std::string s(s_);
ar & s;
}
}
template<class Archive>
void load(Archive& ar, const unsigned version) {
bool isNull;
ar & isNull;
if (!isNull) {
std::string s;
ar & s;
s_ = strdup(s.c_str());
} else {
s_ = 0;
}
}
BOOST_SERIALIZATION_SPLIT_MEMBER();
private:
char*& s_;
};
其使用的一个简单示例如下。
#include "SerializeCStringHelper.hpp"
#include <assert.h>
#include <fstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
int main()
{
const char* fileName = "saved.txt";
const char* str = "This is an example a C-string";
// Save data
{
// Create an output archive
std::ofstream ofs(fileName);
boost::archive::text_oarchive ar(ofs);
// Save
SerializeCStringHelper helper(str);
ar & helper;
}
// Restore data
char* restored_str;
{
// Create and input archive
std::ifstream ifs(fileName);
boost::archive::text_iarchive ar(ifs);
// Load
SerializeCStringHelper helper(restored_str);
ar & helper;
}
// Make sure we read exactly what we saved
assert(restored_str!= str);
assert(strcmp(restored_str, str) == 0);
return 0;
}
非侵入式序列化
到目前为止,序列化代码都添加在类定义中。在类外部进行非侵入式序列化可能更受欢迎。例如,我们希望序列化一个库中的类而不修改该库的*.hpp*文件。当数据成员是公共的时,这很容易。
#pragma once
class Obj {
public:
Obj() : d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : d1_(d1), d2_(d2) {}
bool operator==(const Obj& o) const {
return d1_ == o.d1_ && d2_ == o.d2_;
}
public:
int d1_;
bool d2_;
};
namespace boost {
namespace serialization {
template<typename Archive>
void serialize(Archive& ar, Obj& o, const unsigned int version) {
ar & o.d1_ & o.d2_;
}
} // namespace serialization
} // namespace boost
如果我们想保护数据成员,代码会更复杂一些,因为序列化模板需要被声明为 `friend`。这需要对模板进行前向声明。
#pragma once
//// Declaration of the template
class Obj;
namespace boost {
namespace serialization {
template<typename Archive>
void serialize(Archive& ar, Obj& o, const unsigned int version);
} // namespace serialization
} // namespace boost
//// Definition of the class
class Obj {
public
Obj() : d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : d1_(d1), d2_(d2) {}
bool operator==(const Obj& o) const {
return d1_ == o.d1_ && d2_ == o.d2_;
}
private:
int d1_;
bool d2_;
// Allow serialization to access data members.
template<typename Archive> friend
void boost::serialization::serialize(Archive& ar, Obj& o,
const unsigned int version);
};
//// Definition of the template
namespace boost {
namespace serialization {
template<typename Archive>
void serialize(Archive& ar, Obj& o, const unsigned int version) {
ar & o.d1_ & o.d2_;
}
} // namespace serialization
} // namespace boost
非侵入式显式保存和加载函数定义
这结合了前两种序列化风格,只是包含文件和宏不同。为了简单起见,我们提供公共数据成员的版本。
#pragma once
#include <boost/serialization/split_free.hpp>
class Obj {
public:
Obj() : d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : d1_(d1), d2_(d2) {}
bool operator==(const Obj& o) const {
return d1_ == o.d1_ && d2_ == o.d2_;
}
public:
int d1_;
bool d2_;
};
namespace boost {
namespace serialization {
template<class Archive>
void save(Archive & ar, const Obj& o, const unsigned int version) {
ar & o.d1_ & o.d2_;
}
template<class Archive>
void load(Archive & ar, Obj& o, const unsigned int version) {
ar & o.d1_ & o.d2_;
}
} // namespace serialization
} // namespace boost
BOOST_SERIALIZATION_SPLIT_FREE(Obj)
STL 容器的序列化
Boost 库提供了模板来自动序列化 STL 容器,以及一些 STL 对象(例如 `std::string`)。而不是使用以下代码保存/加载 vector:
template<typename Archive>
void save(Archive& ar, const std::vector<Obj>& objs, const unsigned version) {
ar << objs.size();
for (size_t i = 0; i < objs.size(); ++i) {
ar << objs[i];
}
}
template<typename Archive>
void load(Archive& ar, std::vector<Obj>& objs, const unsigned version) {
size_t size;
ar >> size;
objs.resize(size);
for (size_t i = 0; i < size; ++i) {
ar >> objs[i];
}
}
您只需编写:
#include <boost/serialization/vector.hpp>
template<typename Archive>
void serialize(Archive& ar, std::vector<Obj>& objs, const unsigned version) {
ar & objs;
}
使用适当的包含文件支持所有 STL 容器。
#include <boost/serialization/array.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/serialization/hash_map.hpp>
#include <boost/serialization/hash_set.hpp>
#include <boost/serialization/list.hpp>
#include <boost/serialization/slist.hpp>
#include <boost/serialization/map.hpp>
#include <boost/serialization/set.hpp>
#include <boost/serialization/bitset.hpp>
#include <boost/serialization/string.hpp>
基类的序列化
当一个类继承自另一个类时,基类也需要被序列化。
#include <boost/serialization/base_object.hpp>
class Base {
public:
Base() : c_('\0') {}
Base(char c) : c_(c) {}
bool operator==(const Base& o) const { return c_ == o.c_; }
private:
char c_;
friend class boost::serialization::access;
template <typename Archive>
void serialize(Archive& ar, const unsigned version) {
ar & c_;
}
};
class Obj : public Base {
private:
typedef Base _Super;
public:
Obj() : _Super(), d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : _Super('a'), d1_(d1), d2_(d2) {}
bool operator==(const Obj& o) const {
return _Super::operator==(o) && d1_ == o.d1_ && d2_ == o.d2_;
}
private:
int d1_;
bool d2_;
friend class boost::serialization::access;
template <typename Archive>
void serialize(Archive& ar, const unsigned version) {
ar & boost::serialization::base_object<_Super>(*this);
ar & d1_ & d2_;
}
};
版本控制
我们希望在 `Obj` 类演进时保持向后兼容性。例如,如果添加了一个新的数据成员 `ID_`,我们希望读取旧存档并构建一个新的 `Obj`,其中缺失的数据成员采用默认值。
#pragma once
#include <boost/serialization/split_member.hpp>
#include <boost/serialization/version.hpp>
class Obj {
public:
Obj() : d1_(-1), d2_(false), ID_(0) {}
Obj(int d1, bool d2, unsigned ID id) : d1_(d1), d2_(d2), ID_(id) {}
bool operator==(const Obj& o) const {
return d1_ == o.d1_ && d2_ == o.d2_ && ID_ == o.ID_;
}
private:
int d1_;
bool d2_;
unsigned ID_;
friend class boost::serialization::access;
template<class Archive>
void save(Archive & ar, const unsigned int version) const {
ar & d1_ & d2_ & ID_;
}
template<class Archive>
void load(Archive & ar, const unsigned int version) {
ar & d1_ & d2_;
// If archive’s version is 0 (i.e., is old), ID_ keeps
// its default value from the new data model,
// else we read ID_’s value from the archive.
if (version > 0) {
ar & ID_;
}
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
};
const 数据或对象的序列化
尝试序列化 const 数据或对象会触发一长串错误消息,其中包括类似以下内容:
[snip]
/opt/local/include/boost/archive/detail/check.hpp:162: error:
invalid application of ‘sizeof’ to incomplete
type ‘boost::STATIC_ASSERTION_FAILURE<false>‘
[snip]
/opt/local/include/boost/archive/basic_text_iprimitive.hpp:88: error:
ambiguous overload for ‘operator>>‘ in
‘((boost::archive::basic_text_iprimitive<std::basic_istream<char,
std::char_traits<char> > >*)this)->
boost::archive::basic_text_iprimitive<std::basic_istream<char,
std::char_traits<char> > >::is >> t’
这意味着输入存档期望数据的接收者是非 const 的。因此,必须使用 `const_cast<>()` 来序列化 const 数据成员。例如:
#pragma once
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
class Obj {
public:
Obj() : d1_(-1), d2_(false) {}
Obj(int d1, bool d2) : d1_(d1), d2_(d2) {}
private:
const int d1_;
bool d2_;
// Allow serialization to access data members.
friend class boost::serialization::access;
template<typename A>
void serialize(A& ar, const unsigned version) {
ar & const_cast<int&>(d1_) & d2_;
}
};
文本、XML 和二进制存档
文本存档是一个人类可读的 ASCII 文件。在 `boost/archive/*.hpp` 中还有其他存档类型可用,例如:
// Text archive that defines boost::archive::text_oarchive
// and boost::archive::text_iarchive
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
// XML archive that defines boost::archive::xml_oarchive
// and boost::archive::xml_iarchive
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
// XML archive which uses wide characters (use for UTF-8 output ),
// defines boost::archive::xml_woarchive
// and boost::archive::xml_wiarchive
#include <boost/archive/xml_woarchive.hpp>
#include <boost/archive/xml_wiarchive.hpp>
// Binary archive that defines boost::archive::binary_oarchive
// and boost::archive::binary_iarchive
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
文本和 XML 存档在 32 位和 64 位平台之间是可移植的。
一个在 32 位和 64 位之间可移植的二进制存档并非易事,因为 C++ 没有规定基本类型的大小。例如,一个 `long` 在 32 位机器上通常是 4 字节,在 64 位机器上是 8 字节。但实际上,它是相当可移植的——有一个非官方的便携式二进制存档版本。