Qt/C++借助QVariant实现可存储通用类型的容器-创新互联
在项目开发过程中,我们可能会遇到这么一种场景:某个或某几个软件组件可以产生许多不同类型的数据,无论是出于性能的考虑,或者是接口简洁性的考虑,这些数据需要被一次性塞到一个类似于数据库的数据容器中。而这个容器将会被众多接收者使用,它们各自从容器中取出自己感兴趣的内容进行处理。此外,不同的接收者可能运行在不同线程中的,这个容器还需要支持复制操作,使得这些接受者在访问时互不干扰。最后,为了便于调试以及数据的离线分析,这个容器还应该支持序列化与反序列化。
成都服务器托管,成都创新互联提供包括服务器租用、绵阳电信机房、带宽租用、云主机、机柜租用、主机租用托管、CDN网站加速、域名与空间等业务的一体化完整服务。电话咨询:028-86922220简而言之,该场景的需求如下:
- 需要一个能够同时存储多种数据类型的容器;
- 该容器需要提供拷贝的功能;
- 该容器需要支持序列化与反序列化。
我们姑且将满足以上需求的容器称为可存储通用类型的容器。本文假设项目中使用了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_mapcontainer;
我们就得到了一个简易的,可同时存储多种数据类型的关联容器。接收者通过key值便能够以正确的方式解析真实的数据类型。
由于QVaraiant
是实现通用类型的容器的核心,需要重点介绍一下。
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
和QMap
的值存入到一个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支持自定义类型的存储。
首先, 我们需要确保自定义的类型满足QMetaType
的所有要求。换句话说,它必须提供:
- 一个公有的默认构造函数;
- 一个公有的拷贝构造函数;
- 一个公有的析构函数。
例如,我们有一个MyData
自定义类型:
class MyData {public:
uint32_t dataId;
std::vectordata;
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
对象中并在以后检索成为可能。官方文档也给出了一个示例代码,请参见自定义类型示例。
如前文提到,我们将使用QDataStream
对QVarant
进行序列化与反序列化。对于自定义类型,我们则需要实现自定义的序列化与反序列化函数。在这个例子中,我们需要实现两个MyData
的友元函数:
class MyData {public:
uint32_t dataId;
std::vectordata;
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(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 stream(...);
MyData data;
data.dataId = 10086;
data.data = {1, 0, 0, 8, 6};
QVariant var;
var.setValue(data);
stream<< var;
若此时我们执行反序列化是可行的,因为在setValue()
时,内部帮我们在元对象系统中注册了这个自定义类型。但如果是我们重新运行程序,反序列化就会失效。反序列化时需要重新创建出这个自定义类型的对象,因为此时元对象系统中并不清楚该对象的信息,所以它不知道如何在运行时处理自定义类型对象的创建和销毁。
要在运行时创建对象,需要通过调用qRegisterMetaType()
模板函数将自定义类型注册到元对象系统。此后,除了让反序列化时元对象系统可识别之外,这也使该类型可用于队列式(queued)信号-槽通信。我们需要在使用该类型的反序列化之前调用它,本例子中,我们选择在MainWindow
的构造函数中进行注册:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{ui->setupUi(this);
qRegisterMetaType();
}
此后,我们便可以使用如下的方式进行反序列化了:
QDataStream stream(...);
QVariant var;
stream >>var;
auto data = qvariant_cast(var);
// do something on data...
3. Reference- QVariant Class
- Creating Custom Qt Types
- QMetaType Class
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
当前文章:Qt/C++借助QVariant实现可存储通用类型的容器-创新互联
分享URL:http://scyanting.com/article/dpicis.html