Qt/C++ 借助QVariant实现可存储通用类型的容器
1. 背景
在项目开发过程中,我们可能会遇到这么一种场景:某个或某几个软件组件可以产生许多不同类型的数据,无论是出于性能的考虑,或者是接口简洁性的考虑,这些数据需要被一次性塞到一个类似于数据库的数据容器中。而这个容器将会被众多接收者使用,它们各自从容器中取出自己感兴趣的内容进行处理。此外,不同的接收者可能运行在不同线程中的,这个容器还需要支持复制操作,使得这些接受者在访问时互不干扰。最后,为了便于调试以及数据的离线分析,这个容器还应该支持序列化与反序列化。
简而言之,该场景的需求如下:
- 需要一个能够同时存储多种数据类型的容器;
- 该容器需要提供拷贝的功能;
- 该容器需要支持序列化与反序列化。
我们姑且将满足以上需求的容器称为可存储通用类型的容器。本文假设项目中使用了Qt库,在此基础上,为实现这种容器提供了一种可行的思路,并给出实现这种容器的要点。
2. 设计思路
2.1. 为单个数据项选择合适的容器
首先,我们需要解决的问题就是如何存储各种类型的单个数据项。我们根据需求逐一分析:
- 需求1:为了满足需求1,最简单的实现思路就是使用
void *
,存储任意类型的数据。 - 需求2:由于需要支持容器的复制,这意味着容器中的每个数据项目也应该逐一被复制到新的容器中,那么需求1中提到的
void *
将无法满足该需求。这是因为void *
擦除了类型信息,在数据项目需要被复制时,我们已经无从得知该数据的长度以及其他信息了。那能否用void *
+ size来存储这些内容呢?对于基础数据类型以及其数组,例如int
、float
以及它们数组等,这种方案是可以应付的。但是,一旦数据项目中使用了容器类,例如std::vector
,因为通常情况下,它们实际的存储空间是在堆中申请的,我们不能直接通过其指针来访问实际的数据内容,因此void *
+ size的方案不可行。C++ 17中引入了std::any
用于存储单个任意类型的对象(包括自定义类型),这似乎是一个很不错的选择。 - 需求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. 让自定义类型支持序列化与反序列化
如前文提到,我们将使用QDataStream
对QVarant
进行序列化与反序列化。对于自定义类型,我们则需要实现自定义的序列化与反序列化函数。在这个例子中,我们需要实现两个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
- QVariant Class
- Creating Custom Qt Types
- QMetaType Class
Qt/C++ 借助QVariant实现可存储通用类型的容器相关推荐
- qt 将不同的数据类型组成一个新类型_SQL 通用数据类型
SQL 通用数据类型 数据类型定义列中存放的值的种类. 数据库表中的每个列都要求有名称和数据类型. SQL 开发人员必须在创建 SQL 表时决定表中的每个列将要存储的数据的类型. 数据类型是一个标签, ...
- Qt Creator将应用程序部署到通用远程Linux设备
Qt Creator将应用程序部署到通用远程Linux设备 将应用程序部署到通用远程Linux设备 Qt用于设备创建部署步骤 使用Qt 5.8或更早版本进行开发 将应用程序部署到通用远程Linux设备 ...
- QT学习笔记(十):通用算法示例
QT学习笔记(十):通用算法示例 std是C++标准库统一使用的命名空间(namespace)的名称,C++标准库中的名字全部都在std这个命名空间中,std也就是英文"standard&q ...
- Google Gson-反序列化列表 class 宾语? (通用类型)
我想通过Google Gson传输列表对象,但是我不知道如何反序列化泛型类型. 在查看此内容后我尝试了什么(BalusC的答案): MyClass mc = new Gson().fromJson(r ...
- 物理设计-如何存储日期类型
下面我们来看一下最常用的一种日期类型,datetime类型,datetime类型的值呢,分为两部分,日期和时间,默认情况下呢,起始以4位的年,2位月,2位日,时分秒,这样格式来存储日期时间值的,在MY ...
- 干货:如何正确描述存储IO类型?
点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 后台回复"k8s",可领取k8s资料 存储系统作为数据 ...
- 【HCIA-cloud】【4】服务器虚拟化之存储资源管理:存储资源类型说明、存储配置模式说明【普通、精简、延迟置零】、虚拟机磁盘类型说明、FusionCompute中操作添加存储
目录一览 说明 存储资源类型 存储虚拟化与华为云计算存储对比 FusionCompute存储资源与存储资源使用对比 FusionCompute中的存储资源类型 物理磁盘 SATA盘 SAS盘 NL-S ...
- Qt编程基础:认识常用的基本类型
前言 上一节已经成功创建了一个Qt项目,接下来就是要在创建好的项目中,添加自己想要的功能.在写代码之前,我们需要掌握Qt的一些基础知识. 语法部分就不用讲了,这系列文章是对有C或C++语言基础的同学展 ...
- GreenDao存储自定义类型对象解决方案(转)
最近公司项目选用GreenDao作为Android客户端本地数据库的对象关系映射框架.对于GreenDao虽然以往也有简单用过,但这还是笔者第一次在实际业务中使用.碰到了题目所述的两个问题,虽然在Tu ...
最新文章
- 枚举可以被子类化以添加新元素吗?
- jQuery的AJAX
- head在linux命令中什么意思,Linux系统中head命令如何使用
- 04-1.jQuery事件与补充each/data
- 985高校校长:未来5年教职工要压缩千人,淘汰20-30%
- android 定义固定数组,Android 图片数组定义和读取
- bootsrap 外边距_Bootstrap 网格系统布局详解
- 一图读懂基于鲲鹏处理器的全栈混合云华为云Stack6.5
- oracle logical standby,Oracle10gR2 Logical Standby(一)概念与原理
- 快速上手!mysql数据恢复的方法
- macOS 锐捷校园网解决方案
- 软件测试复盘思路个人总结
- java 短信验证码 安全_有关java短信验证码的小知识
- 华人教授世界一流大学观察报告:斯坦福师生吃饭时,谈论的都是什么话题?...
- 【Python打印图形问题】利用print打印一些规则的图形(通过特殊符号比如*和空格组成)
- 开放低代码的钉钉,能否普惠1700万企业?
- win7无声音显示“未插入扬声器或耳机” 怎么解决
- pdf 模版 汉字和数字_标准格式!田字格里如何写汉字和数字(强烈推荐收藏)...
- 系统安全与应用【下】
- 【Swish】Mac 触控板手势窗口管理工具