错误内容大致都是一些clip不支持<
为了解决编译错误&#xff0c;我们则需要将类MyClip支持输入输出操作符>>和<
inline std::istream& operator>>(std::istream& st, MyClip& clip){ st >> clip.mValid; st >> clip.mIn >> clip.mOut; st >> clip.mFilePath; return st;}inline std::ostream& operator<
为了能正常访问类对象的私有成员变量&#xff0c;我们还需要在自定义类型里面增加序列化和反序列化的友元函数(回忆一下这里为何必须使用友元函数而不能直接重载操作符>>和<)&#xff0c;如
friend std::istream& operator>>(std::istream& st, MyClip& clip);friend std::ostream& operator<
这种序列化的实现方法是非常直观而且容易理解的&#xff0c;但缺陷是对于大型的项目开发中&#xff0c;由于自定义类型的数量较多&#xff0c;可能达到成千上万个甚至更多时&#xff0c;对于每个类型我们则需要实现2个函数&#xff0c;一个是序列化转储数据&#xff0c;另一个则是反序列化恢复数据&#xff0c;不仅仅增加了开发实现的代码数量&#xff0c;如果后期一旦对部分类的成员变量有所修改&#xff0c;则需要同时修改这2个函数。
同时考虑到更复杂的自定义类型&#xff0c;比如含有继承关系和自定义类型的成员变量
class MyVideo : public MyClip{ std::list mFilters;};
上述代码需要转储-恢复类MyVideo的对象内容时&#xff0c;事情会变得更复杂些&#xff0c;因为还需要转储-恢复基类&#xff0c;同时成员变量使用了STL模板容器list与自定义类&#39;MyFilter&#96;的结合&#xff0c;这种情况也需要自己去定义转储-恢复的实现方式。
针对以上疑问&#xff0c;有没有一种方法能减少我们代码修改的工作量&#xff0c;同时又易于理解和维护呢&#xff1f;
Boost序列化库
对于使用C&#43;&#43;标准输入输出的方法遇到的问题&#xff0c;好在Boost提供了一种良好的解决方式&#xff0c;则是将所有类型的转储-恢复操作抽象到一个函数中&#xff0c;易于理解&#xff0c;如对于上述类型&#xff0c;只需要将上述的2个友元函数替换为下面的一个友元函数
template friend void serialize(Archive&, MyClip&, unsigned int const);
友元函数的实现类似下面的样子
templatevoid serialize(A &ar, MyClip &clip, unsigned int const ver){ ar & BOOST_SERIALIZATION_NVP(clip.mValid); ar & BOOST_SERIALIZATION_NVP(clip.mIn); ar & BOOST_SERIALIZATION_NVP(clip.mOut); ar & BOOST_SERIALIZATION_NVP(clip.mFilePath);}
其中BOOST_SERIALIZATION_NVP是Boost内部定义的一个宏&#xff0c;其主要作用是对各个变量进行打包。
转储-恢复的使用则直接作用于操作符>>和<
// storeMyClip clip;······std::ostringstream ostr;boost::archive::text_oarchive oa(ostr);oa <> clip;
这里使用的std::istringstream和std::ostringstream即是分别从字符串流中恢复数据以及将类对象的数据转储到字符串流中。
对于类MyFilter和MyVideo则使用相同的方式&#xff0c;即分别增加一个友元模板函数serialize的实现即可&#xff0c;至于std::list模板类&#xff0c;boost已经帮我们实现了。
这时我们发现&#xff0c;对于每一个定义的类&#xff0c;我们需要做的仅仅是在类内部声明一个友元模板函数&#xff0c;同时类外部实现这个模板函数即可&#xff0c;对于后期类的成员变量的修改&#xff0c;如增加、删除或者重命名成员变量&#xff0c;也仅仅是修改一个函数即可。
Boost序列化库已经足够完美了&#xff0c;但故事并未结束&#xff01;
在用于端上开发时&#xff0c;我们发现引用Boost序列化库遇到了几个挑战
- 端上的编译资料很少&#xff0c;官方对端上编译的资料基本没有&#xff0c;在切换不同的版本进行编译时经常会遇到各种奇怪的编译错误问题
- Boost在不同的C&#43;&#43;开发标准之间兼容性不够好&#xff0c;尤其是使用libc&#43;&#43;标准进行编译链接时遇到的问题较多
- Boost增加了端上发行包的体积
- Boost每次序列化都会增加序列化库及版本号等私有头信息&#xff0c;反序列化时再重新解析&#xff0c;降低了部分场景下的使用性能
基于泛型编程的序列化实现方法为了解决使用Boost遇到的这些问题&#xff0c;我们觉得有必要重新实现序列化库&#xff0c;以剥离对Boost的依赖&#xff0c;同时能满足如下要求
- 由于现有工程大量使用了Boost序列化库&#xff0c;因此兼容现有的代码以及开发者的习惯是首要目标
- 尽量使得代码修改和重构的工作量最小
- 兼容不同的C&#43;&#43;开发标准
- 提供比Boost序列化库更高的性能
- 降低端上发行包的体积
为了兼容现有使用Boost的代码以及保持当前开发者的习惯&#xff0c;同时使用代码修改的重构的工作量最小&#xff0c;我们应该保留模板函数serialize&#xff0c;同时对于模板函数内部的实现&#xff0c;为了提高效率也不需要对各成员变量重新打包&#xff0c;即直接使用如下定义
#define BOOST_SERIALIZATION_NVP(value) value
对于转储-恢复的接口调用&#xff0c;仍然延续目前的调用方式&#xff0c;只是将输入输出类修改为
alivc::text_oarchive oa(ostr);alivc::text_iarchive ia(istr);
好了&#xff0c;到此为止&#xff0c;序列化库对外的接口工作已经做好&#xff0c;剩下的就是内部的事情&#xff0c;应该如何重新设计和实现序列化库的内部框架才能满足要求呢&#xff1f;
先来看一下当前的设计架构的处理流程图
比如对于转储类text_oarchive&#xff0c;其支持的接口必须包括
explicit text_oarchive(std::ostream& ost, unsigned int version &#61; 0);template text_oarchive& operator开发者调用操作符函数<
template text_oarchive& operator<
当开始对具体类型的各个成员进行操作时&#xff0c;这时需要进行判断&#xff0c;如果此成员变量的类型已经是内建类型&#xff0c;则直接进行序列化&#xff0c;如果是自定义类型&#xff0c;则需要重新回调到对应类型的模板函数serialize中
template text_oarchive& operator&(T& v){ basic_save::invoke(*this, v, mversion); return *this;}
上述代码中的basic_save::invoke则会在编译期完成模板类型推导并选择直接对内建类型进行转储还是重新回调到成员变量对应类型的serialize函数继续重复上述过程。
由于内建类型数量有限&#xff0c;因此这里我们选择使模板类basic_save的默认行为为回调到相应类型的serialize函数中
template struct basic_load_save{ template static void invoke(A& ar, T& v, unsigned int version) { serialize(ar, v, version); }};template struct basic_save : public basic_load_save::value>{};
这时会发现上述代码的模板参数多了一个参数E&#xff0c;这里主要是需要对枚举类型进行特殊处理&#xff0c;使用偏特化的实现如下
template struct basic_load_save{ template static void invoke(A& ar, T& v, unsigned int version) { int tmp &#61; v; ar & tmp; v &#61; (T)tmp; }};
到这里我们已经完成了重载操作符&的默认行为&#xff0c;即是不断进行回溯到相应的成员变量的类型中的模板函数serialize中&#xff0c;但对于碰到内建模型时&#xff0c;我们则需要让这个回溯过程停止&#xff0c;比如对于int类型
template struct basic_pod_save{ template static void invoke(A& ar, T const& v, unsigned int) { ar.template save(v); }};template <>struct basic_save : public basic_pod_save{};
这里对于int类型&#xff0c;则直接转储整数值到输出流中&#xff0c;此时text_oarchive则还需要增加一个终极转储函数
template void save(T const& v){ most <
这里我们发现&#xff0c;在save成员函数中&#xff0c;我们已经将具体的成员变量的值输出到流中了。
对于其它的内建类型&#xff0c;则使用相同的方式处理&#xff0c;要以参考C&#43;&#43; std::basic_ostream的源码实现。
相应的&#xff0c;对于恢复操作的text_iarchive的操作流程如下图
测试结果
我们对使用Boost以及重新实现的序列化库进行了对比测试&#xff0c;其结果如下
代码修改的重构的工作非常小&#xff0c;只需要删除Boost的相关头文件&#xff0c;以及将boost相关命名空间替换为alivc&#xff0c;BOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替换Android端下的发行包体积大概减少了500KB目前的消息处理框架中&#xff0c;处理一次消息的平均时间由100us降低到了25us代码实现约300行&#xff0c;更轻量级
未来还能做什么
由于当前项目的原因&#xff0c;重新实现的序列化还没有支持转储-恢复指针所指向的内存数据&#xff0c;但当前的设计框架已经考虑了这种拓展性&#xff0c;未来会考虑支持。
总结
泛型编程能够大幅提高开发效率&#xff0c;尤其是在代码重用方面能发挥其优势&#xff0c;同时由于其类型推导及生成代码均在编译期完成&#xff0c;并不会降低性能序列化对于需要进行转储-恢复的解耦处理以及协助定位异常和崩溃的原因分析具有重要作用利用C&#43;&#43;及模板自身的语言特性优势&#xff0c;结合合理的架构设计&#xff0c;即易于拓展又能尽量避免过度设计
参考资料
https://www.ibm.com/developerworks/cn/aix/library/au-boostserialization/
作者&#xff1a;lifesider