长时间燃烧午夜机油后开发的软件在客户现场崩溃。 您无所事事,因为没有可用的测试用例来帮助您复制崩溃并调试出了什么问题。 这是许多人熟悉的情况,但是不断出现的问题是,对此可以做些什么? 仅转储堆栈跟踪信息并不是最佳选择。 您需要深入了解代码的数据结构以检查其值。
解决方案是Boost序列化库。 您可以使用它将程序内容转储到归档文件(文本或XML文件)中,并从同一归档文件中还原数据,以在崩溃之前重新创建代码的精确快照。 听起来不错? 继续阅读。
序列化源随附于标准的Boost安装(请参阅参考资料 )。 与许多其他Boost库不同,序列化不是仅标头库,因此您需要构建它。 为此,请查看安装随附的构建说明(请参阅参考资料 )。 如果你喜欢一个现成的,货架安装,看看成boostpro
(再次参见相关主题 )。 出于本文的目的,我使用了Boost版本1.46.1,并使用gcc-4.3.4编译了代码。
带有Boost序列化的Hello World
在继续进行更大的事情之前,让我们创建一个概念证明。 在下面的清单1中,您将看到一个字符串,其值已转储到存档中。 在下面的清单2中,您将还原同一档案的内容,以验证字符串的值是否与原始值匹配。
清单1.将字符串的内容保存到文本档案中
#include <boost/archive/text_oarchive.hpp>
#include <iostream>
#include <fstream>
void save()
{
std::ofstream file("archive.txt");
boost::archive::text_oarchive oa(file);
std::string s = "Hello World!\n";
oa << s;
}
int main()
{
save();
}
现在,您将内容加载回去。
清单2.将字符串的内容加载到文本档案中
#include <boost/archive/text_iarchive.hpp>
#include <iostream>
#include <fstream>
void load()
{
std::ifstream file("archive.txt");
boost::archive::text_iarchive ia(file);
std::string s;
ia >> s;
std::cout << s << std::endl;
}
int main()
{
load();
}
可以预期,清单2的输出是“ Hello World”。
现在,让我们仔细看一下代码。 Boost将创建您要转储的内容的文本存档(文本文件)。 要转储内容,请创建一个text_oarchive
。 要恢复内容,请分别在标头text_oarchive.hpp和text_iarchive.hpp中创建一个text_iarchive
声明。 转储和还原内容非常直观,使用<<
和>>
运算符,其工作方式与流I / O完全相同,只是将内容转储到文件中并在以后从同一文件还原。
但是,您可能希望对转储和还原使用相同的&
运算符,而不是使用这两个不同的运算符。 下面的清单3显示了如何。
清单3.使用&运算符进行转储还原
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <iostream>
#include <fstream>
void save()
{
std::ofstream file("archive.txt");
boost::archive::text_oarchive oa(file);
std::string s = "Hello World!\n";
oa & s; // has same effect as oa << s;
}
void load()
{
std::ifstream file("archive.txt");
boost::archive::text_iarchive ia(file);
std::string s;
ia & s;
std::cout << s << std::endl;
}
让我们看一下转储的文本文件:
22 serialization::archive 9 13 Hello World!
请注意,文本文件的内容和格式可能会在将来的Boost版本中更改,因此,任何依赖于内部存档内容的应用程序代码都是一个坏主意。
创建一个XML存档
如果要使用XML存档而不是文本存档,则必须包含Boost源中的标头xml_iarchive.hpp和xml_oarchive.hpp。 这些头声明或定义XML归档语义。 但是,转储还原与您对文本存档所做的操作略有不同:数据需要包装在名为BOOST_SERIALIZATION_NVP
的宏中。 下面的清单4提供了代码。
清单4.从XML存档中转储-还原
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <iostream>
#include <fstream>
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
std::string s = "Hello World!\n";
oa & BOOST_SERIALIZATION_NVP(s);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
std::string s;
ia & BOOST_SERIALIZATION_NVP(s);
std::cout << s << std::endl;
}
清单5显示了XML存档的内容。 变量名称用作标签( <s>Hello World!</s>
)。
清单5. XML档案的内容
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<s>Hello World!
</s>
</boost_serialization>
您还能序列化什么?
这是好东西开始的地方。 您可以序列化C++
编程语言的许多元素,而无需进行过多编码。 类,指向类的指针,数组和标准模板库(STL)集合都可以序列化。 下面的清单6提供了一个带有数组的示例。
清单6.转储-恢复整数数组
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
int array1[] = {34, 78, 22, 1, 910};
oa & BOOST_SERIALIZATION_NVP(array1);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
int restored[5]; // Need to specify expected array size
ia >> BOOST_SERIALIZATION_NVP(restored);
std::ostream_iterator<int> oi(std::cout, " ");
std::copy(a, a+5, oi);
}
int main()
{
save();
load();
}
那很简单。 转储与字符串类完全一样。 但是,在还原期间,您需要指定预期的阵列大小。 否则,程序最终将崩溃。 清单7提供了清单6中代码的转储XML档案。
清单7.数组转储创建的XML存档
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<array1>
<count>5</count>
<item>34</item>
<item>78</item>
<item>22</item>
<item>1</item>
<item>910</item>
</array1>
</boost_serialization>
您是否可以仅通过指定指针int* restored
来完成此操作,并为您恢复数组? 最简洁的答案是不。 必须始终指定大小。 但是,长的答案是序列化指向原始类型的指针是不平凡的。
序列化STL集合
要序列化STL列表和向量,您必须了解,对于每种STL类型,应用程序代码都必须包括与序列化源中名称相似的头文件。 对于列表,您包括boost / serialization / list.hpp,依此类推。 请注意,使用列表和向量,在信息回载期间无需提供任何大小或范围,这是另一个理由,与具有相同功能的应用程序容器相比,更喜欢STL容器。 下面的清单8显示了用于序列化STL集合的代码。
清单8.序列化STL集合
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/serialization/list.hpp>
#include <boost/serialization/vector.hpp>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
float array[] = {34.2, 78.1, 22.221, 1.0, -910.88};
std::list<float> L1(array, array+5);
std::vector<float> V1(array, array+5);
oa & BOOST_SERIALIZATION_NVP(L1);
oa & BOOST_SERIALIZATION_NVP(V1);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
std::list<float> L2;
ia >> BOOST_SERIALIZATION_NVP(L2); // No size/range needed
std::vector<float> V2;
ia >> BOOST_SERIALIZATION_NVP(V2); // No size/range needed
std::ostream_iterator<float> oi(std::cout, " ");
std::copy(L2.begin(), L2.end(), oi);
std::copy(V2.begin(), V2.end(), oi);
}
清单9显示了XML归档的外观。
清单9.使用STL容器时转储的档案
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<L1>
<count>5</count>
<item_version>0</item_version>
<item>34.200001</item>
<item>78.099998</item>
<item>22.221001</item>
<item>1</item>
<item>-910.88</item>
</L1>
<V1>
<count>5</count>
<item_version>0</item_version>
<item>34.200001</item>
<item>78.099998</item>
<item>22.221001</item>
<item>1</item>
<item>-910.88</item>
</V1>
</boost_serialization>
序列化自己的类型
您要序列化自己的类型吗? 是! 让我们举一个代表日期的结构的小例子:
typedef struct date {
unsigned int m_day;
unsigned int m_month;
unsigned int m_year;
} date;
要使一个类序列化,您需要定义一个称为serialize
的方法作为类定义的一部分。 在转储以及类还原期间调用此方法。 这是serialize
方法的声明:
template<class Archive>
void serialize(Archive& archive, const unsigned int version)
{
//… your custom code here
}
从第二个片段中,您可以看到serialize
是一个模板函数,并且第一个参数应该是对Boost存档的引用。 那么,XML归档的代码应该是什么样的? 这是您输入的内容:
template<class Archive>
void serialize(Archive& archive, const unsigned int version)
{
archive & BOOST_SERIALIZATION_NVP(m_day);
archive & BOOST_SERIALIZATION_NVP(m_month);
archive & BOOST_SERIALIZATION_NVP(m_year);
}
下面的清单10提供了完整的代码。
清单10.日期类型的转储-还原
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <iostream>
#include <fstream>
typedef struct date {
unsigned int m_day;
unsigned int m_month;
unsigned int m_year;
date( int d, int m, int y) : m_day(d), m_month(m), m_year(y)
{}
date() : m_day(1), m_month(1), m_year(2000)
{}
friend std::ostream& operator << (std::ostream& out, date& d)
{
out << "day: " << d.m_day
<< " month: " << d.m_month
<< " year: " << d.m_year;
return out;
}
template<class Archive>
void serialize(Archive& archive, const unsigned int version)
{
archive & BOOST_SERIALIZATION_NVP(m_day);
archive & BOOST_SERIALIZATION_NVP(m_month);
archive & BOOST_SERIALIZATION_NVP(m_year);
}
} date;
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
date d(15, 8, 1947);
oa & BOOST_SERIALIZATION_NVP(d);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
date dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
std::cout << dr;
}
请注意,除了定义serialize
方法之外,您在处理用户定义的类型上没有做任何特殊的事情。 上面的代码可以正常工作,但是存在一个明显的问题:您可能需要序列化来自第三方的类型,并且其类声明不能被修改。 在这种情况下,应使用在类范围之外定义的serialize
的非侵入式版本。 下面的清单11显示了date
类的非侵入式serialize
方法。 请注意,如果在全局范围内定义了serialize
方法,则该代码仍然有效。 但是,在相关命名空间中定义方法是一种良好的编码习惯。
清单11. serialize方法的非介入版本
namespace boost {
namespace serialization {
template<class Archive>
void serialize(Archive& archive, date& d, const unsigned int version)
{
archive & BOOST_SERIALIZATION_NVP(d.m_day);
archive & BOOST_SERIALIZATION_NVP(d.m_month);
archive & BOOST_SERIALIZATION_NVP(d.m_year);
}
} // namespace serialization
} // namespace boost
清单12显示了日期类型的XML档案。
清单12.用户定义类型的XML归档
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<d class_id="0" tracking_level="0" version="0">
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d>
</boost_serialization>
处理类层次结构
类通常是从其他类派生的,因此您需要一种在序列化派生类的同时处理基类序列化的方法。 对于基类和派生类,必须定义serialize
方法。 另外,您需要调整派生类的serialize
定义,如清单13所示。
清单13.序列化基类
template<class Archive>
void serialize(Archive& archive, const unsigned int version)
{
// serialize base class information
archive & boost::serialization::base_object<Base Class>(*this);
// serialize derived class members
archive & derived-class-member1;
archive & derived-class-member2;
// …
}
直接在派生类的serialize
方法内部调用基类的serialize
方法是一个非常糟糕的主意。 也许可行,但是无法跟踪类版本控制(稍后描述)或消除生成的归档文件中的冗余。 为避免此类错误,建议使用一种编码样式,该方法是将serialize
方法在所有类中都设为私有,并在要serialize
所有类中使用声明friend class boost::serialization::access
。
通过基类指针转储派生类
通过指针转储派生类是完全可能的。 但是,基类和派生类都应定义各自的serialize
方法。 另外,您需要调用该方法...
<archive name>.register_type<derived-type name>( )
。 。 。 在转储以及还原过程中。 您应该假定date
类是从称为base
类派生的。 清单14显示了应该在save
和load
方法中编写的代码。
清单14.使用基类指针进行序列化
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
oa.register_type<date>( );
base* b = new date(15, 8, 1947);
oa & BOOST_SERIALIZATION_NVP(b);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
ia.register_type<date>( );
base *dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
date* dr2 = dynamic_cast<date*> (dr);
std::cout << dr2;
}
在此,转储和还原期间都将使用base
指针。 但是,实际上是序列化的date
对象。 您已经在转储之前注册了日期类型,并且在两种情况下都将其还原。
在转储-还原过程中使用指向对象的指针
使用指向对象的指针可以转储和还原。 这样做使事情变得有趣。 您期望XML归档内容是什么? 显然,转储指针值不会成功。 您需要将实际对象转储并稍后还原。 另外,关于同一对象的多个指针呢? 如果XML存档中转储了同一对象的多个副本,那么显然不是最佳选择。 Boost序列化的妙处在于,语法几乎在所有地方都相同,包括指针的语法。 下面的清单15是清单10的修改版本。
清单15.使用指针进行转储-还原
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
date* d = new date(15, 8, 1947);
std::cout << d << std::endl;
oa & BOOST_SERIALIZATION_NVP(d);
// … other code follows
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
date* dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
std::cout << dr << std::endl;
std::cout << *dr;
}
请注意,在此清单中, d
和dr
的值不同,但是内容相同。 清单16显示了清单15中代码的XML档案。
清单16.带有指针用法的XML归档
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<d class_id="0" tracking_level="1" version="0" object_id="_0">
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d>
</boost_serialization>
现在,考虑一种情况,其中您将两个指针转储到同一对象,并观察相同对象的存档。 清单17显示了清单15中代码的略微修改版本。
清单17.使用指针进行转储-还原
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
date* d = new date(15, 8, 1947);
std::cout << d << std::endl;
oa & BOOST_SERIALIZATION_NVP(d);
date* d2 = d;
oa & BOOST_SERIALIZATION_NVP(d2);
// … other code follows
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
date* dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
std::cout << dr << std::endl;
std::cout << *dr;
date* dr2;
ia >> BOOST_SERIALIZATION_NVP(dr2);
std::cout << dr2 << std::endl;
std::cout << *dr2;
}
下面的清单18提供了清单17中的代码的XML归档。 观察如何处理第二个指针; 同样,仅转储了一个对象。
清单18.以d2为指针的XML档案
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<d class_id="0" tracking_level="1" version="0" object_id="_0">
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d>
<d2 class_id_reference="0" object_id_reference="_0"></d2>
</boost_serialization>
关于引用,用户应用程序代码中的处理完全相同。 但是,请注意,在还原期间,将创建两个唯一的对象。 因此,归档文件还应包含两个具有相同值的对象。 与指针的情况不同,如果d2
是清单17中的引用,则归档文件的外观如下(请参见下面的清单19 )。
清单19. XML档案,其中d2是对d的引用
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<d class_id="0" tracking_level="0" version="0">
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d>
<d2>
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d2>
</boost_serialization>
将序列化分为保存和加载
有时候,您不想使用相同的serialize
方法来转储和还原对象。 在这种情况下,您可以将serialize
方法分为两个方法(适当地命名为save
和load
并具有相似的签名。 这两种方法都是先前定义serialize
的同一类的一部分。 另外,您需要添加宏BOOST_SERIALIZATION_SPLIT_MEMBER
作为类定义的一部分。 清单20显示了这些方法的样子。
清单20.将序列化拆分为save和load方法
template<class Archive>
void save(Archive& archive, const unsigned int version) const
{
//…
}
template<class Archive>
void load(Archive& archive, const unsigned int version)
{
//…
}
BOOST_SERIALIZATION_SPLIT_MEMBER( ) // must be part of class
在save
方法签名后注意const
。 没有const
限定词,代码将无法编译。 对于您的date
类, 清单21显示了这些方法的外观。
清单21.日期类的保存和加载方法
template<class Archive>
void save(Archive& archive, const unsigned int version) const
{
archive << BOOST_SERIALIZATION_NVP(m_day);
archive << BOOST_SERIALIZATION_NVP(m_month);
archive << BOOST_SERIALIZATION_NVP(m_year)
}
template<class Archive>
void load(Archive& archive, const unsigned int version)
{
archive >> BOOST_SERIALIZATION_NVP(m_day);
archive >> BOOST_SERIALIZATION_NVP(m_month);
archive >> BOOST_SERIALIZATION_NVP(m_year)
}
BOOST_SERIALIZATION_SPLIT_MEMBER( ) // must be part of class
了解版本控制
serialize
, save
和load
的方法签名具有无符号整数版本作为最后一个参数。 这个号码是做什么用的? 随着时间的流逝,类可能会更改其内部变量名称,添加新字段或删除现有字段,等等。 除了存档保留有关数据类型的旧状态的信息外,这是软件开发过程的自然发展。 要解决此问题,请使用版本号。
让我们以date
类为例。 假设您在date
类中引入了一个新的名为m_tag
的string
类型的字段。 根据清单12 ,该类的先前版本已作为版本0转储到归档文件中。 下面的清单22显示了该类的load
方法(您可以使用serialize
但在这里使用load
make可以实现更简洁的实现)。
清单22.使用版本控制来处理较新的类字段
template<class Archive>
void load(Archive& archive, const unsigned int version)
{
archive >> BOOST_SERIALIZATION_NVP(m_day);
archive >> BOOST_SERIALIZATION_NVP(m_month);
archive >> BOOST_SERIALIZATION_NVP(m_year);
if (version > 0)
archive >> BOOST_SERIALIZATION_NVP(m_tag);
}
显然,正确使用版本控制可使代码与软件早期版本中使用的旧档案一起使用。
使用共享指针
共享指针是一种经常使用但非常强大的编程技术。 Boost序列化的主要优点之一是,再次轻松地对共享指针进行序列化和其语法与到目前为止所学的相同。 唯一需要注意的是,您必须在应用程序代码中包含头文件boost / serialization / shared_ptr.hpp。 首先修改清单15,并使用boost::shared_ptr
而不是普通的指针。 代码如清单23所示。
清单23.使用共享指针进行转储-还原
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
boost::shared_ptr<date> d (new date(15, 8, 1947));
oa & BOOST_SERIALIZATION_NVP(d);
// … other code follows
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
boost::shared_ptr<date> dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
std::cout << *dr;
}
序列化是万恶的灵丹妙药吗? 还没。 序列化支持和不支持的用法存在使用限制。 例如,如果在实际对象本身之前转储了指向堆栈中对象的指针,则Boost序列化将崩溃。 首先需要转储对象,然后转储指向该对象的指针(请注意,指针可以作为独立对象转储而无需转储该对象)。 以清单24中的示例为例。
清单24.指向堆栈上对象的指针需要在实际对象之后转储
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
date d(15, 8, 1947);
std::cout << d << std::endl;
date* d2 = &d;
oa & BOOST_SERIALIZATION_NVP(d);
oa & BOOST_SERIALIZATION_NVP(d2);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
date dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
std::cout << dr << std::endl;
date* dr2;
ia >> BOOST_SERIALIZATION_NVP(dr2);
std::cout << dr2 << std::endl;
}
在此清单中,您不能在d
之前转储d2
。 如果您查看XML档案,这将变得更加清楚: d2
被转储为对d
的引用(请参见清单25 )。
清单25.一个对象及其指针都被转储的XML档案
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<d class_id="0" tracking_level="1" version="0" object_id="_0">
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d>
<d2 class_id_reference="0" object_id_reference="_0"></d2>
</boost_serialization>
如果有多个指向同一对象的指针,则序列化会将指针与具有class_id_reference
的原始对象相关联(每个对象都有唯一的类ID)。 指向原始对象的每个后续指针都将object_id_reference
更改为_1
, _2
,依此类推。
结论
本文就是这样。 您已经了解了什么是Boost序列化。 如何创建和使用文本和XML档案; 以及如何转储和还原普通的旧数据类型(STL集合,类,指向类的指针,共享指针和数组)。 本文还简要介绍了使用序列化和版本控制处理类层次结构。 序列化是一个强大的工具供您使用。 在您的代码中充分利用它可以简化调试体验。
翻译自: https://www.ibm.com/developerworks/aix/library/au-boostserialization/index.html