1. 背景

在项目开发过程中,我们可能会遇到这么一种场景:某个或某几个软件组件可以产生许多不同类型的数据,无论是出于性能的考虑,或者是接口简洁性的考虑,这些数据需要被一次性塞到一个类似于数据库的数据容器中。而这个容器将会被众多接收者使用,它们各自从容器中取出自己感兴趣的内容进行处理。此外,不同的接收者可能运行在不同线程中的,这个容器还需要支持复制操作,使得这些接受者在访问时互不干扰。最后,为了便于调试以及数据的离线分析,这个容器还应该支持序列化与反序列化。

简而言之,该场景的需求如下:

  1. 需要一个能够同时存储多种数据类型的容器;
  2. 该容器需要提供拷贝的功能;
  3. 该容器需要支持序列化与反序列化。

我们姑且将满足以上需求的容器称为可存储通用类型的容器。本文假设项目中使用了Qt库,在此基础上,为实现这种容器提供了一种可行的思路,并给出实现这种容器的要点。

2. 设计思路

2.1. 为单个数据项选择合适的容器

首先,我们需要解决的问题就是如何存储各种类型的单个数据项。我们根据需求逐一分析:

  1. 需求1:为了满足需求1,最简单的实现思路就是使用void *,存储任意类型的数据。
  2. 需求2:由于需要支持容器的复制,这意味着容器中的每个数据项目也应该逐一被复制到新的容器中,那么需求1中提到的void *将无法满足该需求。这是因为void *擦除了类型信息,在数据项目需要被复制时,我们已经无从得知该数据的长度以及其他信息了。那能否用void * + size来存储这些内容呢?对于基础数据类型以及其数组,例如intfloat以及它们数组等,这种方案是可以应付的。但是,一旦数据项目中使用了容器类,例如std::vector,因为通常情况下,它们实际的存储空间是在堆中申请的,我们不能直接通过其指针来访问实际的数据内容,因此void * + size的方案不可行。C++ 17中引入了std::any用于存储单个任意类型的对象(包括自定义类型),这似乎是一个很不错的选择。
  3. 需求3:std::any本身并没有直接支持序列化与反序列化功能,如果每个基础类型都需要自己再实现一次序列化与反序列,那实在太折腾了。既然我们都已经基于Qt库进行开发了,那是不是可以直接利用Qt中已经提供的各种序列化与反序列化功能(通过QDataStream)。我们知道,Qt Core模块中的QVariant类提供了与std::any相似的功能,并且更加强大。更幸运地是,在Serializing Qt Data Types列表中,QVarant赫然在列。

综上,为了避免重复造轮子,我们选择了QVariant作为存储单个数据项目的容器。该类支持对象之间的拷贝,因此,选择QVariant让我们可以同时满足3个需求,前提是我们使用的数据类型都是Qt内置的类型。对于自定义类型,我们还需要做一些简单的开发工作,这将在后面的小节中介绍。

在确定了单个数据项目的容器之后,我们还需要选择一个容器来存储多个数据项目,这里假设使用std::unordered_map<>,那么,我们只需要按照如下方式定义:

std::unordered_map<int, QVariant> container;

我们就得到了一个简易的,可同时存储多种数据类型的关联容器。接收者通过key值便能够以正确的方式解析真实的数据类型。

由于QVaraiant是实现通用类型的容器的核心,需要重点介绍一下。

2.1.1. QVariant

QVariant类的作用类似于Qt数据类型的联合,它还能支持用户自定义的类型。

QVariant对象一次保存一个type()的单个值。(有些type()是多值的,例如字符串列表。)我们可以使用convert()将其转换为不同的类型,使用众多toT()函数之一(例如,toSize())获取其值,并使用canConvert()检查该类型是否可以转换为某个特定类型。

名为toT()(例如toInt()toString())的方法是const方法。如果想要获取实际存储的类型,这些函数会返回存储对象的副本(注意,这里返回的是副本,所以无法通过返回的值来修改QVariant存储的内容)。如果想使用可以从存储的类型生成的类型,toT()会复制并转换,并保持对象本身不变。如果请求了一个不能从存储的类型生成的类型,结果取决于该类型;有关的详细信息,请参见函数文档。

下列是官方的实例代码,它们阐述了如何使用QVariant

QDataStream out(...);
QVariant v(123);                // The variant now contains an int
int x = v.toInt();              // x = 123
out << v;                       // Writes a type tag and an int to out
v = QVariant("hello");          // The variant now contains a QByteArray
v = QVariant(tr("hello"));      // The variant now contains a QString
int y = v.toInt();              // y = 0 since v cannot be converted to an int
QString s = v.toString();       // s = tr("hello")  (see QObject::tr())
out << v;                       // Writes a type tag and a QString to out
...
QDataStream in(...);            // (opening the previously written stream)
in >> v;                        // Reads an Int variant
int z = v.toInt();              // z = 123
qDebug("Type is %s",            // prints "Type is int"v.typeName());
v = v.toInt() + 100;            // The variant now hold the value 223
v = QVariant(QStringList());

甚至可以将QList<QVariant>QMap<QString, QVariant>的值存入到一个QVariant对象中,因此,我们可以轻松地构造任意类型的复杂的数据结构,并将其存入到QVariant中。这是非常强大和通用的,但可能比在标准数据结构中存储相应类型的内存和速度效率低。

QVariant还支持null的概念,在这种情况下,我们可以定义一个没有值的类型。但是,请注意,QVariant类型只有在设置了值后才能进行强制转换。例如:

QVariant x, y(QString()), z(QString(""));
x.convert(QVariant::Int);
// x.isNull() == true
// y.isNull() == true, z.isNull() == false

除了支持内置的类型枚举中的类型之外,QVariant可以通过扩展以支持其他类型。如何实现让QVariant可识别的类型,参看让QVariant支持自定义类型的存储。

2.2. 让QVariant支持自定义类型的存储

首先, 我们需要确保自定义的类型满足QMetaType的所有要求。换句话说,它必须提供:

  • 一个公有的默认构造函数;
  • 一个公有的拷贝构造函数;
  • 一个公有的析构函数。

例如,我们有一个MyData自定义类型:

class MyData {public:uint32_t dataId;std::vector<uint32_t> data;MyData() = default;~MyData() = default;MyData(const MyData &) = default;MyData & operator=(const MyData &) = default;
};

在此基础上,我们还需要做一些额外的简单操作,否则,Qt的类型系统将无法理解如何存储、检索和序列化该类的实例。例如,我们将无法在QVariant中存储MyData值。

Qt中负责定制类型的类是QMetaType。为了让这个类能识别这个类型,我们在定义MyData的头文件中调用这个类的Q_DECLARE_METATYPE()宏,代码如下:

Q_DECLARE_METATYPE(MyData);

这使得将MyData对象存储在QVariant对象中并在以后检索成为可能。官方文档也给出了一个示例代码,请参见自定义类型示例。

2.3. 让自定义类型支持序列化与反序列化

如前文提到,我们将使用QDataStreamQVarant进行序列化与反序列化。对于自定义类型,我们则需要实现自定义的序列化与反序列化函数。在这个例子中,我们需要实现两个MyData的友元函数:

class MyData {public:uint32_t dataId;std::vector<uint32_t> data;MyData() = default;~MyData() = default;MyData(const MyData &) = default;MyData & operator=(const MyData &) = default;friend QDataStream & operator<<(QDataStream & stream, const MyData &data);friend QDataStream & operator>>(QDataStream & stream, MyData &data);
};QDataStream & operator<<(QDataStream & stream, const MyData &data)
{stream << data.dataId;stream << static_cast<quint64>(data.data.size());for (const auto & val : data.data) {stream << val;}return stream;
}QDataStream & operator>>(QDataStream & stream, MyData &data)
{stream >> data.dataId;quint64 count = 0;stream >> count;uint32_t val = 0;for (int i = 0; i < count; i++) {stream >> val;data.data.push_back(val);}return stream;
}

在完成以上代码后,该自定义类型便能支持基于QDataStream的序列化和反序列化了。但是,因为我们将自定义类型放入了QVariant中,在序列化时,实际上直接的序列化对象是QVariant,它此时并不知道该自定义类型的流操作符。因此,我们还需要使用qRegisterMetaTypeStreamOperators()来使能QVariant的对自定义类型的序列化与反序列(在Qt 6之后,该函数已被废弃,它被更方便的qRegisterMetaType<MyData>()替代)。该函数将自定义类型的流运算符重载函数注册到元对象系统中,这让QVariant被序列化/反序列化时,知道该如何调用自定义类型的流运算符重载函数。相关代码如下:

qRegisterMetaTypeStreamOperators<MyData>();QDataStream stream(...);MyData data;
data.dataId = 10086;
data.data = {1, 0, 0, 8, 6};
QVariant var;
var.setValue(data);
stream << var;QVariant var1;
stream >> var1;
auto data = qvariant_cast<MyData>(var);
// do something on data...

3. Reference

  1. QVariant Class
  2. Creating Custom Qt Types
  3. QMetaType Class

Qt/C++ 借助QVariant实现可存储通用类型的容器相关推荐

  1. qt 将不同的数据类型组成一个新类型_SQL 通用数据类型

    SQL 通用数据类型 数据类型定义列中存放的值的种类. 数据库表中的每个列都要求有名称和数据类型. SQL 开发人员必须在创建 SQL 表时决定表中的每个列将要存储的数据的类型. 数据类型是一个标签, ...

  2. Qt Creator将应用程序部署到通用远程Linux设备

    Qt Creator将应用程序部署到通用远程Linux设备 将应用程序部署到通用远程Linux设备 Qt用于设备创建部署步骤 使用Qt 5.8或更早版本进行开发 将应用程序部署到通用远程Linux设备 ...

  3. QT学习笔记(十):通用算法示例

    QT学习笔记(十):通用算法示例 std是C++标准库统一使用的命名空间(namespace)的名称,C++标准库中的名字全部都在std这个命名空间中,std也就是英文"standard&q ...

  4. Google Gson-反序列化列表 class 宾语? (通用类型)

    我想通过Google Gson传输列表对象,但是我不知道如何反序列化泛型类型. 在查看此内容后我尝试了什么(BalusC的答案): MyClass mc = new Gson().fromJson(r ...

  5. 物理设计-如何存储日期类型

    下面我们来看一下最常用的一种日期类型,datetime类型,datetime类型的值呢,分为两部分,日期和时间,默认情况下呢,起始以4位的年,2位月,2位日,时分秒,这样格式来存储日期时间值的,在MY ...

  6. 干货:如何正确描述存储IO类型?

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 后台回复"k8s",可领取k8s资料 存储系统作为数据 ...

  7. 【HCIA-cloud】【4】服务器虚拟化之存储资源管理:存储资源类型说明、存储配置模式说明【普通、精简、延迟置零】、虚拟机磁盘类型说明、FusionCompute中操作添加存储

    目录一览 说明 存储资源类型 存储虚拟化与华为云计算存储对比 FusionCompute存储资源与存储资源使用对比 FusionCompute中的存储资源类型 物理磁盘 SATA盘 SAS盘 NL-S ...

  8. Qt编程基础:认识常用的基本类型

    前言 上一节已经成功创建了一个Qt项目,接下来就是要在创建好的项目中,添加自己想要的功能.在写代码之前,我们需要掌握Qt的一些基础知识. 语法部分就不用讲了,这系列文章是对有C或C++语言基础的同学展 ...

  9. GreenDao存储自定义类型对象解决方案(转)

    最近公司项目选用GreenDao作为Android客户端本地数据库的对象关系映射框架.对于GreenDao虽然以往也有简单用过,但这还是笔者第一次在实际业务中使用.碰到了题目所述的两个问题,虽然在Tu ...

最新文章

  1. 枚举可以被子类化以添加新元素吗?
  2. jQuery的AJAX
  3. head在linux命令中什么意思,Linux系统中head命令如何使用
  4. 04-1.jQuery事件与补充each/data
  5. 985高校校长:未来5年教职工要压缩千人,淘汰20-30%
  6. android 定义固定数组,Android 图片数组定义和读取
  7. bootsrap 外边距_Bootstrap 网格系统布局详解
  8. 一图读懂基于鲲鹏处理器的全栈混合云华为云Stack6.5
  9. oracle logical standby,Oracle10gR2 Logical Standby(一)概念与原理
  10. 快速上手!mysql数据恢复的方法
  11. macOS 锐捷校园网解决方案
  12. 软件测试复盘思路个人总结
  13. java 短信验证码 安全_有关java短信验证码的小知识
  14. 华人教授世界一流大学观察报告:斯坦福师生吃饭时,谈论的都是什么话题?...
  15. 【Python打印图形问题】利用print打印一些规则的图形(通过特殊符号比如*和空格组成)
  16. 开放低代码的钉钉,能否普惠1700万企业?
  17. win7无声音显示“未插入扬声器或耳机” 怎么解决
  18. pdf 模版 汉字和数字_标准格式!田字格里如何写汉字和数字(强烈推荐收藏)...
  19. 系统安全与应用【下】
  20. 【Swish】Mac 触控板手势窗口管理工具

热门文章

  1. 个人微信小程序和企业微信小程序的区别
  2. My PC Internet--物理层
  3. 计算机网络巡检员年终总结,网络巡检工作总结.docx
  4. js 银行卡正则校验
  5. python 除法运算
  6. 子沐课堂——Flask小帅锅勾搭Ajax萌妹纸
  7. Flutter的Stateless Widgets
  8. mremote怎么配置远程连接服务器,Windows远程桌面连接的利器-mRemote
  9. Handlebar嵌套遍历数据
  10. php pear 安装扩展,windows上应用pear安装php扩展