Qt/C++借助QVariant实现可存储通用类型的容器-创新互联

1. 背景

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

成都服务器托管,成都创新互联提供包括服务器租用、绵阳电信机房、带宽租用、云主机、机柜租用、主机租用托管、CDN网站加速、域名与空间等业务的一体化完整服务。电话咨询:028-86922220

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

  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_mapcontainer;

我们就得到了一个简易的,可同时存储多种数据类型的关联容器。接收者通过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());

甚至可以将QListQMap的值存入到一个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::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对象中并在以后检索成为可能。官方文档也给出了一个示例代码,请参见自定义类型示例。

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

如前文提到,我们将使用QDataStreamQVarant进行序列化与反序列化。对于自定义类型,我们则需要实现自定义的序列化与反序列化函数。在这个例子中,我们需要实现两个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
  1. QVariant Class
  2. Creating Custom Qt Types
  3. QMetaType Class

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


当前文章:Qt/C++借助QVariant实现可存储通用类型的容器-创新互联
分享URL:http://scyanting.com/article/dpicis.html