基于泛型编程的序列化实现方法

写在前面

序列化是一个转储-恢复的操作过程,即支持将一个对象转储到临时缓冲或者永久文件中和恢复临时缓冲或者永久文件中的内容到一个对象中等操作,其目的是可以在不同的应用程序之间共享和传输数据,以达到跨应用程序、跨语言和跨平台的解耦,以及当应用程序在客户现场发生异常或者崩溃时可以即时保存数据结构各内容的值到文件中,并在发回给开发者时再恢复数据结构各内容的值以协助分析和定位原因。

泛型编程是一个对具有相同功能的不同类型的抽象实现过程,比如STL的源码实现,其支持在编译期由编译器自动推导具体类型并生成实现代码,同时依据具体类型的特定性质或者优化需要支持使用特化或者偏特化及模板元编程等特性进行具体实现。

Hello World

#include <iostream>
int main(int argc, char* argv[])
{std::cout << "Hello World!" << std::endl;return 0;
}

泛型编程其实就在我们身边,我们经常使用的std和stl命名空间中的函数和类很多都是泛型编程实现的,如上述代码中的std::cout即是模板类std::basic_ostream的一种特化

namespace std
{typedef basic_ostream<char>         ostream;
}

从C++的标准输入输出开始

除了上述提到的std::coutstd::basic_ostream外,C++还提供了各种形式的输入输出模板类,如std::basic_istream std::basic_ifstreamstd::basic_ofstream, std::basic_istringstreamstd::basic_ostringstream等等,其主要实现了内建类型(built-in)的输入输出接口,比如对于Hello World可直接使用于字符串,然而对于自定义类型的输入输出,则需要重载实现操作符>><<,如对于下面的自定义类

class MyClip
{bool        mValid;int         mIn;int         mOut;std::string mFilePath;
};

如使用下面的方式则会出现一连串的编译错误

MyClip clip;
std::cout << clip;

错误内容大致都是一些clip不支持<<操作符并在尝试将clip转为cout支持的一系列的内建类型如void*int等等类型时转换操作不支持等信息。

为了解决编译错误,我们则需要将类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<<(std::ostream& st, MyClip const& clip)
{st << clip.mValid << ' ';st << clip.mIn << ' ' << clip.mOut << ' ';st << clip.mFilePath << ' ';return st;
}

为了能正常访问类对象的私有成员变量,我们还需要在自定义类型里面增加序列化和反序列化的友元函数(回忆一下这里为何必须使用友元函数而不能直接重载操作符>><<),如

friend std::istream& operator>>(std::istream& st, MyClip& clip);
friend std::ostream& operator<<(std::ostream& st, MyClip const& clip);

这种序列化的实现方法是非常直观而且容易理解的,但缺陷是对于大型的项目开发中,由于自定义类型的数量较多,可能达到成千上万个甚至更多时,对于每个类型我们则需要实现2个函数,一个是序列化转储数据,另一个则是反序列化恢复数据,不仅仅增加了开发实现的代码数量,如果后期一旦对部分类的成员变量有所修改,则需要同时修改这2个函数。

同时考虑到更复杂的自定义类型,比如含有继承关系和自定义类型的成员变量

class MyVideo : public MyClip
{std::list<MyFilter> mFilters;
};

上述代码需要转储-恢复类MyVideo的对象内容时,事情会变得更复杂些,因为还需要转储-恢复基类,同时成员变量使用了STL模板容器list与自定义类'MyFilter`的结合,这种情况也需要自己去定义转储-恢复的实现方式。

针对以上疑问,有没有一种方法能减少我们代码修改的工作量,同时又易于理解和维护呢?

Boost序列化库

对于使用C++标准输入输出的方法遇到的问题,好在Boost提供了一种良好的解决方式,则是将所有类型的转储-恢复操作抽象到一个函数中,易于理解,如对于上述类型,只需要将上述的2个友元函数替换为下面的一个友元函数

template<typename Archive> friend void serialize(Archive&, MyClip&, unsigned int const);

友元函数的实现类似下面的样子

template<typename A>void 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内部定义的一个宏,其主要作用是对各个变量进行打包。

转储-恢复的使用则直接作用于操作符>><<,比如

// store
MyClip clip;
······
std::ostringstream ostr;
boost::archive::text_oarchive oa(ostr);
oa << clip;// load
std::istringstream istr(ostr.str());
boost::archive::text_iarchive ia(istr);
ia >> clip;

这里使用的std::istringstreamstd::ostringstream即是分别从字符串流中恢复数据以及将类对象的数据转储到字符串流中。

对于类MyFilterMyVideo则使用相同的方式,即分别增加一个友元模板函数serialize的实现即可,至于std::list模板类,boost已经帮我们实现了。

这时我们发现,对于每一个定义的类,我们需要做的仅仅是在类内部声明一个友元模板函数,同时类外部实现这个模板函数即可,对于后期类的成员变量的修改,如增加、删除或者重命名成员变量,也仅仅是修改一个函数即可。

Boost序列化库已经足够完美了,但故事并未结束!

在用于端上开发时,我们发现引用Boost序列化库遇到了几个挑战

  • 端上的编译资料很少,官方对端上编译的资料基本没有,在切换不同的版本进行编译时经常会遇到各种奇怪的编译错误问题
  • Boost在不同的C++开发标准之间兼容性不够好,尤其是使用libc++标准进行编译链接时遇到的问题较多
  • Boost增加了端上发行包的体积
  • Boost每次序列化都会增加序列化库及版本号等私有头信息,反序列化时再重新解析,降低了部分场景下的使用性能

基于泛型编程的序列化实现方法

为了解决使用Boost遇到的这些问题,我们觉得有必要重新实现序列化库,以剥离对Boost的依赖,同时能满足如下要求

  • 由于现有工程大量使用了Boost序列化库,因此兼容现有的代码以及开发者的习惯是首要目标
  • 尽量使得代码修改和重构的工作量最小
  • 兼容不同的C++开发标准
  • 提供比Boost序列化库更高的性能
  • 降低端上发行包的体积

为了兼容现有使用Boost的代码以及保持当前开发者的习惯,同时使用代码修改的重构的工作量最小,我们应该保留模板函数serialize,同时对于模板函数内部的实现,为了提高效率也不需要对各成员变量重新打包,即直接使用如下定义

#define BOOST_SERIALIZATION_NVP(value)  value

对于转储-恢复的接口调用,仍然延续目前的调用方式,只是将输入输出类修改为

alivc::text_oarchive oa(ostr);
alivc::text_iarchive ia(istr);

好了,到此为止,序列化库对外的接口工作已经做好,剩下的就是内部的事情,应该如何重新设计和实现序列化库的内部框架才能满足要求呢?

先来看一下当前的设计架构的处理流程图

比如对于转储类text_oarchive,其支持的接口必须包括

explicit text_oarchive(std::ostream& ost, unsigned int version = 0);
template <typename T> text_oarchive& operator<<(T& v);
template <typename T> text_oarchive& operator&(T& v);

开发者调用操作符函数<<时,需要首先回调到相应类型的模板函数serialize

template <typename T>
text_oarchive& operator<<(T& v)
{serialize(*this, v, mversion);return *this;
}

当开始对具体类型的各个成员进行操作时,这时需要进行判断,如果此成员变量的类型已经是内建类型,则直接进行序列化,如果是自定义类型,则需要重新回调到对应类型的模板函数serialize

template <typename T>
text_oarchive& operator&(T& v)
{basic_save<T>::invoke(*this, v, mversion);return *this;
}

上述代码中的basic_save::invoke则会在编译期完成模板类型推导并选择直接对内建类型进行转储还是重新回调到成员变量对应类型的serialize函数继续重复上述过程。

由于内建类型数量有限,因此这里我们选择使模板类basic_save的默认行为为回调到相应类型的serialize函数中

template <typename T, bool E = false>
struct basic_load_save
{template <typename A>static void invoke(A& ar, T& v, unsigned int version){serialize(ar, v, version);}
};template <typename T>
struct basic_save : public basic_load_save<T, std::is_enum<T>::value>
{
};

这时会发现上述代码的模板参数多了一个参数E,这里主要是需要对枚举类型进行特殊处理,使用偏特化的实现如下

template <typename T>
struct basic_load_save<T, true>
{template <typename A>static void invoke(A& ar, T& v, unsigned int version){int tmp = v;ar & tmp;v = (T)tmp;}
};

到这里我们已经完成了重载操作符&的默认行为,即是不断进行回溯到相应的成员变量的类型中的模板函数serialize中,但对于碰到内建模型时,我们则需要让这个回溯过程停止,比如对于int类型

template <typename T>
struct basic_pod_save
{template <typename A>static void invoke(A& ar, T const& v, unsigned int){ar.template save(v);}
};template <>
struct basic_save<int> : public basic_pod_save<int>
{
};

这里对于int类型,则直接转储整数值到输出流中,此时text_oarchive则还需要增加一个终极转储函数

template <typename T>
void save(T const& v)
{most << v << ' ';
}

这里我们发现,在save成员函数中,我们已经将具体的成员变量的值输出到流中了。

对于其它的内建类型,则使用相同的方式处理,要以参考C++ std::basic_ostream的源码实现。

相应的,对于恢复操作的text_iarchive的操作流程如下图

测试结果

我们对使用Boost以及重新实现的序列化库进行了对比测试,其结果如下

  • 代码修改的重构的工作非常小,只需要删除Boost的相关头文件,以及将boost相关命名空间替换为alivcBOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替换
  • Android端下的发行包体积大概减少了500KB
  • 目前的消息处理框架中,处理一次消息的平均时间由100us降低到了25us
  • 代码实现约300行,更轻量级

未来还能做什么

由于当前项目的原因,重新实现的序列化还没有支持转储-恢复指针所指向的内存数据,但当前的设计框架已经考虑了这种拓展性,未来会考虑支持。

总结

  • 泛型编程能够大幅提高开发效率,尤其是在代码重用方面能发挥其优势,同时由于其类型推导及生成代码均在编译期完成,并不会降低性能
  • 序列化对于需要进行转储-恢复的解耦处理以及协助定位异常和崩溃的原因分析具有重要作用
  • 利用C++及模板自身的语言特性优势,结合合理的架构设计,即易于拓展又能尽量避免过度设计


原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/519724.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微服务架构下,解决数据一致性问题的实践

随着业务的快速发展&#xff0c;应用单体架构暴露出代码可维护性差、容错率低、测试难度大和敏捷交付能力差等诸多问题&#xff0c;微服务应运而生。微服务的诞生一方面解决了上述问题&#xff0c;但是另一方面却引入新的问题&#xff0c;其中主要问题之一就是&#xff1a;如何…

2019阿里云开年Hi购季满返活动火热报名中!

2019阿里云云上采购季活动已经于2月25日正式开启&#xff0c;从已开放的活动页面来看&#xff0c;活动分为三个阶段&#xff1a; 2月25日-3月04日的活动报名阶段、3月04日-3月16日的新购满返5折抢购阶段、3月16日-3月31日的续费抽豪礼5折抢购阶段。 整个大促活动包含1个主会场…

2019云计算高光时刻:乱云飞渡 传统IT大溃败

前言&#xff1a;2019年&#xff0c;物理机最后一张王牌也败给了云计算&#xff0c;无论从成本还是性能的角度&#xff0c;都没有不选云计算的理由&#xff0c;这是一个时代的终结。 2019的云计算市场格局&#xff0c;依旧是马太效应凸显、大者恒大的趋势继续&#xff0c;但在…

java 集成 kafka 0.8.2.1 适配jdk1.6

文章目录一、版本说明二、实战2.1. 依赖2.2. 生产者代码2.3. 消费端代码2.4. 测试三、小伙伴疑难解答3.1. 首先新建一个maven项目3.2. 把我的依赖和代码复制过去3.3. 把我写的case调试通3.4. 找到左边External Libraries3.5. jar处理3.6. 打开非maven项目&#xff0c;添加jar3.…

阿里云MWC 2019发布7款重磅产品,助力全球企业迈向智能化

当地时间2月25日&#xff0c;在巴塞罗那举行的MWC 2019上&#xff0c;阿里云面向全球发布了7款重磅产品&#xff0c;涵盖无服务器计算、高性能存储、全球网络、企业级数据库、大数据计算等主要云产品&#xff0c;可满足电子商务、物流、金融科技以及制造等各行业企业的数字化转…

linux环境安装 kafka 0.8.2.1 jdk1.6

文章目录一、环境分布二、实战1. kafka下载2. 解压3. 配置4. 编写启动脚本5. 编写关闭脚本6. 赋予脚本可执行权限7. 脚本使用案例三、Config配置四、Consumer配置五、Producer配置很多小伙伴问我&#xff0c;为什么不用最新版本的kafka呢&#xff1f;关于这个问题&#xff0c;都…

元旦限时特惠,耳机、书籍等大降价

戳蓝字“CSDN云计算”关注我们哦&#xff01;今天是12月31日离2020年仅有不到一天的时间你们的2019年目标都实现了吗&#xff1f;在这一年你写了多少行代码改了多少个bug呢&#xff1f;2020年的愿望是否也是希望自己写的代码bug能少一些&#xff1f;小编的2020年希望能买到更多…

ant编译web项目

文章目录1.下载ant2. 解压ant3. 配置an环境变量4. 验证二、编译项目2.1. 新建一个build.xml2.2. 编译项目测试1.下载ant 官网链接&#xff1a; https://ant.apache.org/srcdownload.cgi 2. 解压ant 3. 配置an环境变量 4. 验证 ant -v二、编译项目 2.1. 新建一个build.xml…

Spark in action on Kubernetes - Playground搭建与架构浅析

前言 Spark是非常流行的大数据处理引擎&#xff0c;数据科学家们使用Spark以及相关生态的大数据套件完成了大量又丰富场景的数据分析与挖掘。Spark目前已经逐渐成为了业界在数据处理领域的行业标准。但是Spark本身的设计更偏向使用静态的资源管理&#xff0c;虽然Spark也支持了…

阿里云发布时间序列数据库TSDB,关于时序你了解多少?

概要介绍 时间序列数据是一种表示物理设备&#xff0c;系统、应用过程或行为随时间变化的数据&#xff0c;广泛应用于物联网&#xff0c;工业物联网&#xff0c;基础运维系统等场景。阿里云TSDB 时间序列数据库可以解决大规模时序数据的可靠写入&#xff0c;降低数据存储成本&…

VMware宣布完成27亿美元收购Pivotal;日本成功研发出6G芯片:单载波速度高达100Gbps;联想手机再换新掌门……...

关注并标星星CSDN云计算 速递、最新、绝对有料。这里有企业新动、这里有业界要闻&#xff0c;打起十二分精神&#xff0c;紧跟fashion你可以的&#xff01;每周两次&#xff0c;打卡即read更快、更全了解泛云圈精彩newsgo go go【1月1日 星期三】云の声音5G医疗爆发箭在弦上&am…

使用Logtail采集Kubernetes上挂载的NAS日志

采集k8s挂载Nas后的日志 该文档主要介绍使用logtail以两种不同的方式进行k8s挂载Nas后的日志采集。两种采集方式的实现原理是一样的&#xff0c;都是通过将Logtail和业务容器挂载到相同的NAS上&#xff0c;使Logtail和业务容器的日志数据共享&#xff0c;以此实现日志采集。下…

linux目录挂载

挂载前声明&#xff1a; 执行挂载后&#xff0c;源本地目录下的文件会不显示。 挂载前&#xff1a;需要提前将原目录下面的日志文件备份转移&#xff0c;挂载成功后&#xff0c;在转移到挂载的本地目录下面即可。操作流程如下&#xff1a; 1. 将/app/fis/xml中148G多的日志文件…

深度揭秘“蚂蚁双链通”

今年年初&#xff0c;蚂蚁金服ATEC城市峰会在上海举行。在ATEC区块链行业研讨会分论坛上&#xff0c;蚂蚁金服区块链高级产品专家杨俊带来了主题为《供应链金融&#xff0c;不止于金融&#xff1a;蚂蚁双链通——基于区块链的供应链协作网络》的精彩分享。 蚂蚁金服区块链高级产…

@程序员,不要瞎努力!比起熬夜更可怕的是“熬日”!

最近&#xff0c;笔者在经常后台看到小伙伴留言在问&#xff0c;想学Python&#xff0c;但不知道如何入门&#xff1f;其实对于这个问题&#xff0c;真是仁者见仁智者见智。有句老话说的好“一千个读者&#xff0c;就有一千个哈姆雷特”不过对于此疑惑&#xff0c;笔者就想直接…

配置nginx作为静态资源服务器 css,js,image等资源直接访问

1.传统的web项目&#xff0c;一般都将静态资源存放在 webroot 的目录下&#xff0c;这样做很方便获取静态资源&#xff0c;但是如果说web项目很大&#xff0c;用户很多&#xff0c;静态资源也很多时&#xff0c;服务器的性能 或许就会很低下了。这种情况下一般都会需要一个静态…

分布式事务中间件 Fescar - 全局写排它锁解读

前言 一般&#xff0c;数据库事务的隔离级别会被设置成 读已提交&#xff0c;已满足业务需求&#xff0c;这样对应在Fescar中的分支&#xff08;本地&#xff09;事务的隔离级别就是 读已提交&#xff0c;那么Fescar中对于全局事务的隔离级别又是什么呢&#xff1f;如果认真阅…

云+X案例展 | 民生类:京东云突破数据中心光互联瓶颈

本案例由京东云投递并参与评选&#xff0c;CSDN云计算独家全网首发&#xff1b;更多关于【云X 案例征集】的相关信息&#xff0c;点击了解详情丨挖掘展现更多优秀案例&#xff0c;为不同行业领域带来启迪&#xff0c;进而推动整个“云行业”的健康发展。随着数字化的进程&#…

UI2Code智能生成Flutter代码--整体设计篇

背景: 随着移动互联网时代的到来&#xff0c;人类的科学技术突飞猛进。然而软件工程师们依旧需要花费大量精力在重复的还原UI视觉稿的工作。 UI视觉研发拥有明显的特征&#xff1a;组件&#xff0c;位置和布局&#xff0c;符合机器学习处理范畴。能否通过机器视觉和深度学习等手…

如何成为优秀的技术主管?你要做到这三点

阿里妹导读&#xff1a;技术主管&#xff0c;又叫「技术经理」&#xff0c;英文一般是 Tech Leader &#xff0c;简称 TL。随着工作经验的不断积累&#xff0c;能力的不断提升&#xff0c;每个人都有机会成为Team Leader。然而在机会到来前&#xff0c;我们必须提前做好准备&am…