原文链接 https://woboq.com/blog/qmetatype-knows-your-types.html Olivier Goffart 于 2015年04月22日
QMetaType 是 Qt 用来在运行时保存和获取类型动态信息的一种机制。它支持很多功能,比如:
用 QVariant 包装自定义类型
在 队列连接(queued connection) 中拷贝参数
以及其他依赖运行时类型信息的场景
如果你曾经疑惑过 Q_DECLARE_METATYPE 或 qRegisterMetaType 到底是做什么的、什么时候该用(或不该用),
那就继续读下去吧。本文将介绍你需要了解的关于 QMetaType 的内容:
它的用途是什么
如何使用它
以及它的工作原理
让我们先从一点历史说起。QMetaType 在 Qt 4.0 中引入,其目的是为了支持异步的信号与槽(Qt::QueuedConnection)。
为了让队列连接的槽函数能够工作,我们必须:
复制信号的参数
将这些参数存储在一个事件中,稍后再处理
在槽函数调用完成后,销毁这些参数副本
(注意:在使用 Qt::DirectConnection 时并不需要这些操作,因为参数直接使用的是栈上的指针。)
在 QMetaObject::activate 中分发信号的代码只持有一个 void* 参数指针数组。
但在那个阶段,Qt 对参数类型的了解 只有类型名字符串,这些字符串是由 moc 生成的。
QMetaType 提供了一种机制,可以从类型名字符串(例如 "QPoint")获得 拷贝或销毁对象所需的函数。
Qt 会使用以下接口来复制和销毁参数:
1void *QMetaType::create(int type, void *copy);2void QMetaType::destroy(int type, void *data);
其中 type 是通过:
xxxxxxxxxx11QMetaType::type(const char *typeName)
由 moc 提供的参数类型名获取的。
此外,QMetaType 也允许开发者将 任意类型注册到元类型数据库中。
QMetaType 的另一个重要使用场景是 QVariant。
在 Qt 3.x 中,QVariant 只支持内置的 Qt 类型,
因为如果要封装任意类型,QVariant 也必须能够在自身生命周期内正确地拷贝和销毁该对象。
而借助 QMetaType,QVariant 终于可以存储 任何已注册的类型——因为现在它知道如何拷贝和销毁这些对象实例了。
自 Qt 4.0 以来,情况发生了很大变化。后来引入了 QtScript 和 QML,它们都大量使用了动态类型集成,因此相关机制也经历了大量优化。
下面是 元类型系统(meta-type system) 中为每一种类型所保存的信息列表:
注册时的类型名
内部会有一个名字索引,用于快速查找对应的 meta type id。
从 Qt 4.7 开始,甚至可以用不同的名字注册同一个类型(这对 typedef 非常有用)。
(拷贝)构造函数和析构函数 包括是否是原地(in-place)构造/析构。
类型大小(size) 用于确定需要分配多少空间,例如在栈上构造或作为内联成员构造时。
标志位(flags)
用来描述与 QTypeInfo 中相同的信息(见下文),或者表示该类型支持的转换方式。
自定义转换函数
通过 QMetaType::registerConverter 注册,用于类型之间的转换。
QMetaObject
如果该类型是一个 QObject 或与之关联,则这里保存对应的元对象数据。
……
QTypeInfo 是一个与 QMetaType 正交(相互独立) 的 trait 类。它允许开发者通过 Q_DECLARE_TYPEINFO 手动指定某个类型的特性,例如:
是否是 可移动的(可以用 memmove 直接搬移)
构造函数 / 析构函数是否必须被调用
这主要用于 容器类的优化,例如 QVector。
举例来说:
隐式共享(implicitly shared) 的类可以通过 memmove 直接移动
而普通的拷贝则必须:
先在拷贝构造函数中增加引用计数
再在析构函数中减少引用计数
C++11 引入了 移动构造函数(move constructor) 和 标准类型 traits 来解决这些问题。
但由于 QTypeInfo 的设计远早于 C++11,而且 Qt 仍然需要兼容较老的编译器,因此只能采用这种方式来实现。
出于历史原因,内置类型和自定义类型在实现上有很大的区别。
对于 QtCore 里的内置类型,每一个元类型相关的函数基本上都是一个 switch,为每种类型写了专门的处理代码。
在 Qt 5.0 中,这部分被大量重构,改为使用模板实现(参见 QMetaTypeSwitcher)。
不过,本文真正关心的是 自定义注册类型 是如何工作的。
对于自定义类型,内部其实很简单:
有一个 QVector<QCustomTypeInfo>,用来保存所有类型的信息,以及一组对应的函数指针。
Q_DECLARE_METATYPE 宏会为某个特定类型 特化 模板类 QMetaTypeId。
实际上,它特化的是 QMetaTypeId2,而大多数代码使用的也是 QMetaTypeId2。
至于为什么要有 QMetaTypeId2,我也不完全清楚,可能是为了让 Qt 能在不破坏旧代码的情况下添加更多内置类型。
QMetaTypeId 的作用是:在编译期确定某个类型对应的 meta-type id。
QMetaTypeId::qt_metatype_id 是 qMetaTypeId<T>() 调用的函数。
在第一次调用这个函数时,它会调用 QMetaType 内部的一些函数,根据宏中指定的类型名,为该类型注册并分配一个 meta-type id;
随后,这个 id 会被存储在一个 static 变量中,以便后续直接使用。
除了类型名之外,其他所有信息都会通过模板由编译器自动推导出来。
使用 Q_DECLARE_METATYPE 声明的类型,实际上是在第一次调用 qMetaTypeId() 时才真正完成注册并分配 id 的。
例如,当一个类型被封装进 QVariant 时,就会触发这一步。
但是,在连接信号与槽时,这种“首次使用”并不会自动发生。
此时,如果你希望该类型可用于信号槽(尤其是 Qt::QueuedConnection),
就需要通过 qRegisterMetaType 来强制触发注册。
开发者经常会忘记注册自己的元类型,直到看到编译错误或运行时错误提示他们需要这样做为止。
但如果 根本不需要手动注册,岂不是更好吗?
实际上,Q_DECLARE_METATYPE 唯一必不可少的原因,是为了 获取类型名。
但在某些情况下,即使没有这个宏,我们也可以在 运行时推导出类型名。
例如:对于 QList<T>,如果 T 已经被注册过,我们就可以查询元类型系统,并通过下面的方式构造类型名:
xxxxxxxxxx11"QList<" + QMetaType::name(qMetaTypeId<T>()) + ">"Qt 对一系列模板类都做了类似的事情,比如:
QList
QVector
QSharedPointer
QPointer
QMap
QHash
…
对于 指向 QObject 子类的指针,我们也可以借助 moc 提供的信息来确定类型名:
xxxxxxxxxx11T::staticMetaObject.className() + "*"另外,从 Qt 5.5 开始,Qt 还会 自动为 Q_GADGET 和 Q_ENUM 声明元类型。
到这里,其实已经不太需要关心 Q_DECLARE_METATYPE 了。
但如果你想把这些类型用在 Q_PROPERTY 中,或者作为 信号槽的参数(尤其是 queued connection),
过去仍然需要显式调用 qRegisterMetaType。
不过从 Qt 5.x 开始,只要 moc 能判断某个类型可以被注册为元类型,
它生成的代码就会自动替你调用 qRegisterMetaType。
在 Qt 5.0 之前,我曾尝试研究:对于那些并不需要类型名的场景,是否可以彻底摆脱 Q_DECLARE_METATYPE。
当时的思路大致是这样的:
xxxxxxxxxx71template<typename T>2struct QMetaTypeId {3 static int qt_metatype_id() {4 static int typeId = QMetaType::registerMetaType(/*...*/);5 return typeId;6 }7};按照 C++ 标准的规定:
对于每一种类型,QMetaTypeId::qt_metatype_id()::typeId 这个静态变量 应该只存在一个实例。
但在实际情况中,一些编译器或链接器 并不严格遵守这一规则。
尤其是在 Windows 平台上,即便使用了正确的导出宏,每个动态库里仍然可能各自拥有一份 typeId 的实例。
这样一来就会出现问题:
同一个类型在不同库中会被注册多次
而我们又 没有一个统一的名称标识符 来识别它
同时我们也 不希望依赖 RTTI
因此,最终的结论是:
在 Qt 5 中,只对那些能够确定类型名的类型进行自动注册。