用C++优雅的实现对象到文件的序列化/反序列化

需求

.  在写代码的过程中,经常会需要把代码层面的对象数据保存到文件,而这些数据会以各种格式存储.例如:json,xml,二进制等等.最近恰好就需要把对象以二进制存储到硬盘.这是一个很简单的需求,相比json,xml格式,二进制是直接把字节copy到硬盘,没有中间商赚差价,所以这实现起来相对容易.

实现

struct Vec3 {
    float x;
    float y;
    float z;
}

.  上面是一个简单的三维向量结构体,如何把它序列化到文件呢?

Vec3 v;
v.x = 1.0f;
v.y = 2.0f;
v.z = 3.0f;
os.write((const char *)&v, sizeof(Vec3));

.  上述是序列化Vec3对象数据到文件的代码,非常直接.它的内存布局是3个浮点型变量紧凑排列,要把它存储到硬盘,只要从头到尾按字节拷贝即可.但是,在实际开发中,要序列化的对象不可能全部都是内存紧凑排列的,例如STL容器.

std::vector<Vec3> vec;

.  如果将容器变量从头到尾拷贝到文件,必然会出现错误.因为容器内部通过一个指针来访问存储的对象,而直接拷贝这个容器,只会把指针拷贝,指针指向的数据却丢失了.但是,容器提供了一个可以直接访问指针指向数据的接口,我们可以通过这个接口得到数据然后直接拷贝.

os.write((const char *)&vec, vec.size() * sizeof(Vec3));        //  错误, 仅拷贝指针
os.write((const char *)vec.data(), vec.size() * sizeof(Vec3));  //  正确, 数据被完全拷贝

.  通过这个方法就可以得到正确的拷贝结果了.通常,好的做法是将序列化和反序列化封装成接口,以便于使用,如何封装接口,就是这篇文章的主题.

.  从上述两个例子可以发现,对于单体对象和数组对象,编写的代码是不一样的,单体对象直接拷贝,数组对象需要通过 .data() 取得数据地址再进行拷贝.而考虑到还有嵌套数组对象 std::vector<std::vector<Vec3>>.对于嵌套数组序列化的代码可能如下:

std::vector<std::vector<Vec3>> vec2;
for (auto & vec: vec2)
{
    os.write((const char *)vec.data(), vec.size() * sizeof(Vec3));
}

.  可以发现,对嵌套数组对象的序列化代码跟上述2种对象又不一样,考虑到还有N层嵌套的数组对象.此外,在C++中有一个可平凡复制的概念,通俗的说,就是可以直接按字节拷贝的结构称之为可平凡复制,上述的Vec3则是一个可平凡复制结构,而STL容器则不是可平凡复制结构,除此之外还有更多不可平凡复制且非容器的结构,故此,如果要封装接口,除了区分单体对象和数组对象,还要区分可平凡复制和不可平凡复制.

void Serialize(std::ostream & os,   const Type & val);  //  序列化
void Deserialize(std::istream & is,       Type & val);  //  反序列化

.  上面是比较理想的接口原型,序列化/反序列化各一个接口,脑补一下,这两个接口的实现应该是怎样的?最直接的实现是对每一种类型重载一个定义,例如:

//  string
void Serialize(std::ostream & os, const std::string & val)
{
    os.write(str.data(), str.size());
}
//  vector<int>
void Serialize(std::ostream & os, const std::vector<int> & val)
{
    os.write(str.data(), str.size() * sizeof(int));
}
//  vector<string>
void Serialize(std::ostream & os, const std::vector<std::string> & val)
{
    for (auto & str: val)
    {
        Serialize(os, str);
    }
}

//  接口调用
std::string str;
std::vector<int> vecint;
std::vector<std::string> vecstr;
Serialize(os, str);
Serialize(os, vecint);
Serialize(os, vecstr);

.  从上面可以看出,接口统一,使用方便.但是对每一种类型都重载,要写的代码实在太多了,万一要序列化一个多层嵌套数组,会写的怀疑人生.借助C++强大的语言特性,这一切都可以一步到位.

//  POD
template <class T, typename std::enable_if_t<std::is_trivially_copyable_v<T>, int> N = 0>
void Serialize(std::ostream & os, const T & val)
{
    os.write((const char *)&val, sizeof(T));
}

//  容器
template <class T, typename std::enable_if_t<
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().begin())> &&
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().end())>, int> N = 0>
    void Serialize(std::ostream & os, const T & val)
{
    unsigned int size = val.size();
    os.write((const char *)&size, sizeof(size));
    for (auto & v : val) { Serialize(os, v); }
}

//  POD
template <class T, typename std::enable_if_t<std::is_trivially_copyable_v<T>, int> N = 0>
void Deserialize(std::istream & is, T & val)
{
    is.read((char *)&val, sizeof(T));
}

//  容器
template <class T, typename std::enable_if_t<
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().begin())> &&
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().end())>, int> N = 0>
    void Deserialize(std::istream & is, T & val)
{
    unsigned int size = 0;
    is.read((char *)&size, sizeof(unsigned int));
    val.resize(size);
    for (auto & v : val) { Deserialize(is, v); }
}

.  以上实现可序列化任意可平凡拷贝结构,并且也可序列化任意嵌套层数的STL风格数组.而对于不可平凡复制结构,只需要针对该结构重载即可.借助C++强大的类型推导机制和SFINEA机制,可保证类型安全又具备可扩展性.

原文地址:https://www.cnblogs.com/mmc1206x/p/11053826.html

时间: 2024-10-01 13:22:53

用C++优雅的实现对象到文件的序列化/反序列化的相关文章

Java将对象写入文件读出——序列化与反序列化

Java类中对象的序列化工作是通过ObjectOutputStream和ObjectInputStream来完成的. 写入: 1 File aFile=new File("e:\\c.txt"); 2 Stu a=new Stu(1, "aa", "1"); 3 FileOutputStream fileOutputStream=null; 4 try { 5 fileOutputStream = new FileOutputStream(aFi

对象写入文件和从文件中重构到对象

//将对象写入文件中 Game game = new Game(); game.Level = 2; game.Player = "Tom"; FileStream fs = new FileStream(@"game.bak",FileMode.OpenOrCreate,FileAccess.Write); StreamWriter sw = new StreamWriter(fs); sw.WriteLine(game.Player); sw.WriteLine

复杂对象写入文件

//简单对象可以通过直接写入文件的方式进行存储,复杂对象我们无法直接写入文件,这个时候需要借助归档和反归档 //归档和反归档并不是数据持久化的方式,而是将复杂对象转化成简单对象的一种方式 Person * per = [Person new];    per.name = @"欧阳冰";    per.gender = @"神秘";    per.hobby = @"美女";    per.age = @"21";      

java中将list、map对象写入文件

链接地址:http://blog.sina.com.cn/s/blog_4a4f9fb50101p6jv.html 推荐:凤爪女瓜子男怪象该谁反思伦敦房价为什么持续暴涨 × wvqusrtg个人中心发博文消息 doudouhe的博客 http://blog.sina.com.cn/dechuan608hewei [订阅][手机订阅] 首页博文目录图片关于我 个人资料 doudouhe 微博 加好友发纸条 写留言加关注 博客等级: 博客积分:842 博客访问:222,144 关注人气:34 获赠金

Windows下Java File对象创建文件夹时的一个"坑"

import java.io.File; import java.io.IOException; public class DirCreate { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub String dirStr="D:"; File dir=new File(dirStr); System.out.println("====

[R] 保存pheatmap图片对象到文件

一般我们使用pheatmap通过Rstudio交互得到的图片在plots的Export导出即可,如何保存对象到文件呢?这个需求在自动化流程中很常见,作者似乎也没说明. 生成示例数据: test = matrix(rnorm(200), 20, 10) test[1:10, seq(1, 10, 2)] = test[1:10, seq(1, 10, 2)] + 3 test[11:20, seq(2, 10, 2)] = test[11:20, seq(2, 10, 2)] + 2 test[1

Java中对文件的序列化和反序列化

public class ObjectSaver { public static void main(String[] args) throws Exception { /*其中的 D:\\objectFile.obj 表示存放序列化对象的文件*/ //序列化对象 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("c:\\test\\objectFile.obj")); Customer cust

文件操作,路径操作,StringIO和BytesIO,序列化反序列化,正则表达式与python中使用

文件操作 打开操作open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True,opener=None)打开一个文件,返回一个文件对象(流对象)和文件描述符.打开文件失败,则返回异常基本使用: 创建一个文件test,然后打开它,用完关闭文件操作中,最常用的操作就是读和写. 文件访问的模式有两种:文本模式和二进制模式.不同模式下,操作函数不尽相同,表现的结果也不一样.open的参数

文件IO序列化及反序列化

序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程.在序列化期间,对象将其当前状态写入到临时或持久性存储区.简单点言之:通过设计的一套协议,按照莫种规则,把内存中的数据保存到文件中或者直通过网络传输.把数据转换为字节序列,输出到文件就是序列化,反正,从文件的字节序列恢复到内存,就是反序列化. 其中就有多种库对此过程进行协助: pickle库:dumps 对象序列化为bytes对象dump 对象序列化为文件对象,就是存入文件loads 从bytes对象反序列化l