QMetaType

原文链接 https://woboq.com/blog/qmetatype-knows-your-types.html Olivier Goffart 于 2015年04月22日

QMetaType 是 Qt 用来在运行时保存和获取类型动态信息的一种机制。它支持很多功能,比如:

如果你曾经疑惑过 Q_DECLARE_METATYPEqRegisterMetaType 到底是做什么的、什么时候该用(或不该用),

那就继续读下去吧。本文将介绍你需要了解的关于 QMetaType 的内容:

 

为什么 Qt 需要运行时的动态类型信息?

让我们先从一点历史说起。QMetaType 在 Qt 4.0 中引入,其目的是为了支持异步的信号与槽(Qt::QueuedConnection)

为了让队列连接的槽函数能够工作,我们必须:

(注意:在使用 Qt::DirectConnection 时并不需要这些操作,因为参数直接使用的是栈上的指针。)

QMetaObject::activate 中分发信号的代码只持有一个 void* 参数指针数组。

参见 https://thinkinginqt.com/20251210_how-qt-signals-and-slots-work1/20251210_how-qt-signals-and-slots-work1.html

但在那个阶段,Qt 对参数类型的了解 只有类型名字符串,这些字符串是由 moc 生成的。

QMetaType 提供了一种机制,可以从类型名字符串(例如 "QPoint")获得 拷贝或销毁对象所需的函数

Qt 会使用以下接口来复制和销毁参数:

其中 type 是通过:

由 moc 提供的参数类型名获取的。

此外,QMetaType 也允许开发者将 任意类型注册到元类型数据库中

QMetaType 的另一个重要使用场景是 QVariant

在 Qt 3.x 中,QVariant 只支持内置的 Qt 类型,

因为如果要封装任意类型,QVariant 也必须能够在自身生命周期内正确地拷贝和销毁该对象。

而借助 QMetaTypeQVariant 终于可以存储 任何已注册的类型——因为现在它知道如何拷贝和销毁这些对象实例了。

 

QMetaType 保存了哪些信息?

Qt 4.0 以来,情况发生了很大变化。后来引入了 QtScriptQML,它们都大量使用了动态类型集成,因此相关机制也经历了大量优化。

下面是 元类型系统(meta-type system) 中为每一种类型所保存的信息列表:

QTypeInfo

QTypeInfo 是一个与 QMetaType 正交(相互独立) 的 trait 类。它允许开发者通过 Q_DECLARE_TYPEINFO 手动指定某个类型的特性,例如:

这主要用于 容器类的优化,例如 QVector

举例来说:

C++11 引入了 移动构造函数(move constructor)标准类型 traits 来解决这些问题。

但由于 QTypeInfo 的设计远早于 C++11,而且 Qt 仍然需要兼容较老的编译器,因此只能采用这种方式来实现。

 

QMetaType 是如何工作的?

出于历史原因,内置类型自定义类型在实现上有很大的区别。

对于 QtCore 里的内置类型,每一个元类型相关的函数基本上都是一个 switch,为每种类型写了专门的处理代码。

Qt 5.0 中,这部分被大量重构,改为使用模板实现(参见 QMetaTypeSwitcher)。

不过,本文真正关心的是 自定义注册类型 是如何工作的。

对于自定义类型,内部其实很简单:

有一个 QVector<QCustomTypeInfo>,用来保存所有类型的信息,以及一组对应的函数指针。

Q_DECLARE_METATYPE 宏

Q_DECLARE_METATYPE 宏会为某个特定类型 特化 模板类 QMetaTypeId

实际上,它特化的是 QMetaTypeId2,而大多数代码使用的也是 QMetaTypeId2

至于为什么要有 QMetaTypeId2,我也不完全清楚,可能是为了让 Qt 能在不破坏旧代码的情况下添加更多内置类型。

QMetaTypeId 的作用是:在编译期确定某个类型对应的 meta-type id

QMetaTypeId::qt_metatype_idqMetaTypeId<T>() 调用的函数。

第一次调用这个函数时,它会调用 QMetaType 内部的一些函数,根据宏中指定的类型名,为该类型注册并分配一个 meta-type id;

随后,这个 id 会被存储在一个 static 变量中,以便后续直接使用。

除了类型名之外,其他所有信息都会通过模板由编译器自动推导出来

qRegisterMetaType

使用 Q_DECLARE_METATYPE 声明的类型,实际上是在第一次调用 qMetaTypeId()才真正完成注册并分配 id 的。

例如,当一个类型被封装进 QVariant 时,就会触发这一步。

但是,在连接信号与槽时,这种“首次使用”并不会自动发生。

此时,如果你希望该类型可用于信号槽(尤其是 Qt::QueuedConnection),

就需要通过 qRegisterMetaType强制触发注册

 

自动注册

开发者经常会忘记注册自己的元类型,直到看到编译错误或运行时错误提示他们需要这样做为止。

但如果 根本不需要手动注册,岂不是更好吗?

实际上,Q_DECLARE_METATYPE 唯一必不可少的原因,是为了 获取类型名

但在某些情况下,即使没有这个宏,我们也可以在 运行时推导出类型名

例如:对于 QList<T>,如果 T 已经被注册过,我们就可以查询元类型系统,并通过下面的方式构造类型名:

Qt 对一系列模板类都做了类似的事情,比如:

对于 指向 QObject 子类的指针,我们也可以借助 moc 提供的信息来确定类型名:

另外,从 Qt 5.5 开始,Qt 还会 自动为 Q_GADGETQ_ENUM 声明元类型

到这里,其实已经不太需要关心 Q_DECLARE_METATYPE 了。

但如果你想把这些类型用在 Q_PROPERTY 中,或者作为 信号槽的参数(尤其是 queued connection)

过去仍然需要显式调用 qRegisterMetaType

不过从 Qt 5.x 开始,只要 moc 能判断某个类型可以被注册为元类型

它生成的代码就会自动替你调用 qRegisterMetaType

 

研究

Qt 5.0 之前,我曾尝试研究:对于那些并不需要类型名的场景,是否可以彻底摆脱 Q_DECLARE_METATYPE

当时的思路大致是这样的:

按照 C++ 标准的规定:

对于每一种类型,QMetaTypeId::qt_metatype_id()::typeId 这个静态变量 应该只存在一个实例

但在实际情况中,一些编译器或链接器 并不严格遵守这一规则

尤其是在 Windows 平台上,即便使用了正确的导出宏,每个动态库里仍然可能各自拥有一份 typeId 的实例。

这样一来就会出现问题:

因此,最终的结论是:

Qt 5 中,只对那些能够确定类型名的类型进行自动注册