原文地址:http://labs.qt.nokia.com/2012/06/22/changes-to-the-meta-object-system-in-qt-5/
Qt 5 的元对象系统作出了一定的改变,既有底层变化,又有 API 的变化。其中有些修改与 Qt 4 不是源代码兼容的。本文将介绍这些改变,以及如何修改现有代码,使其能够使用 Qt 5 进行编译。同时,我们也将阐述下新增加的一些 API,使 QMetaMethod
更方便使用。
移除 Qt 4 元对象系统的支持
元对象数据(例如,moc 生成的)有一个版本号,用于描述元对象的内容(格式/布局)和特性。当我们向新的主要版本的 Qt 的元对象增加新特性时(例如让一个类的构造函数支持反射),我们必须更新这个元数据版本号。Qt 4 最新的元对象版本是 6。
为了保持向后兼容,新的小版本的 Qt 更新必须支持早期版本的元对象。这是由 Qt 内部在恰当位置检查元对象版本来实现的。如果元对象是旧的,那么就要切换到早期版本的代码。
因为 Qt 5 已经不与 Qt 4 二进制兼容,因此,我们选择不支持旧的元对象系统了。也就是说,前面我们说的保持旧有代码在 Qt 5 中已经没有了,因为 Qt 不再使用 Qt 4 所使用的那个版本的元对象了。Qt 5 的元对象版本号是 7。
Qt 4 的 moc 输出代码也不能使用 Qt 5 编译。如果你的代码中有硬编码的 moc 输出代码(希望没有,不过 Qt 自己倒是使用了一些,用于实现“特殊的”元对象),你必须自己修改这些代码。
Qt 的很多运行时的元对象构建器(QMetaObjectBuilder、QtDBus、ActiveQt 等)也必须修改代码,以便兼容第 7 版元对象。不过,因为这些代码通常是内部使用的,因此不会影响到使用这些库的程序。(希望没人自己编写了一个元对象构建器…)
函数不再使用字符串识别
在 Qt 5 之前的版本,元对象包含了完整的、一般化的类信号、槽以及Q_INVOKABLE
函数,将其作为以 0 为终止符的字符串进行存储。这在对于完全基于字符串的QObject::connect()
是必须的(使用SIGNAL()
和SLOT()
宏)。但是现在QObject::connect()
是基于模板的,将函数的完全签名作为字符串存储就不那么明智了。(Qt 4 内部处理这些字符串供 QObject::connectNotify()
使用,我们会在后文详细说明。)
函数反射的另外一个通用使用是动态绑定(QML、QScript)。在这种情况下,一个字符串的签名就不那么合适。如果能够直接访问函数名和参数类型,由于减少了运行时解析字符串,会更为简单和有效。
注意,Qt 5 中,QMetaMethod
有了新的函数:name()
、parameterCount()
和parameterType(int index)
。完整的函数签名已经不在元对象数据中存储了(只有函数名),因为正确的签名可以从上面所说的各个函数返回的信息拼接出来。
不幸的是,现在的QMetaMethod::signature()
函数返回一个const char *
,这将会引起内存泄露。我们不能简单地将返回类型修改成QByteArray
,因为类似如下代码的隐式类型转换将会使程序崩溃,但是却不会有任何警告:
// 如果 signature() 返回 QByteArray,在下面语句执行过后,返回值将会超出作用域(也就会被销毁) const char *sig = someMethod.signature(); // 试试看使用 sig 做些处理吧!
为了解决这个问题,我们在 Qt 5 引入一个新的函数,QMetaMethod::methodSignature()
。旧的QMetaMethod::signature()
函数不再使用。这样,如果你不修改函数名,就会引发一个编译器错误。现有代码应该修改成QMetaMethod::methodSignature()
。那些新的函数,例如QMetaMethod::name()
,相比完整函数签名,在某些情况下也会更好用,比如在调试的时候。
connectNotify()
和disconnectNotify()
QObject::connectNotify()
和disconnectNotify()
是两个很少被重写的虚函数。这两个函数适用在任何 slot 连接或者断开你的类的 signal 的时候。一个潜在的使用情景是,实现隐藏代理的时候,将一个内部对象(后端)连接到一个 public 的 signal 上面。qtsystems 模块就大量使用了这一特性。
在 Qt 5 之前,connectNotify()
为了配合基于字符串的QObject::connect()
函数,其参数是函数签名的const char *
,用于识别出连接到的是哪一个 signal。在基于模板的QObject::connect()
以及自定义的连接,例如 QML 或 QtScript 中,让 Qt 准备一个以字符串形式表达的 signal,只需要调用一个通常没有重写的虚函数。
另外,基于char *
的connectNotify()
非常容易引发一些错误。即便你能够拼写正确每一个函数签名,你也有可能陷入下面的陷阱(这样的问题代码甚至出现在 Qt 内部):
void MyClass::connectNotify(const char *signal) { if (signal == SIGNAL(mySignal())) { // 这永远不会执行,因为比较的是指针而不是字符串内容
文档说,你应该将 signal 参数包装成QLatin1String
,但经常会忘记这么做。Qt 5 的解决方案则不会有这种问题。
在 Qt 5 中,QObject::connectNotify()
和disconnectNotify()
接受一个QMetaMethod
,而不是const char *
。QMetaMethod
仅仅包含两个数据:QMetaObject
指针和索引。这就不会有任何问题,偏向某种连接方式。
这种变化同时允许我们将对connectNotify()
和disconnectNotify()
的调用转移到内部实现的基于索引的connect()
和disconnect()
函数(QMetaObject::connect()
,该函数在 Qt 内部有好几处使用)。在实际应用中,这意味着你重新实现的connectNotify()
函数将会按照你期望的方式调用,即使QObject::connect()
没有显式调用(例如connectSlotsByName()
)。最后,我们可以丢弃掉某些 Qt 中现存的代码,例如那些手工调用的connectNotify(const char *)
,转而使用基于索引的connect()
。
已经重写了connectNotify()
和disconnectNotify()
的现有代码需要迁移到新的 API。有两个函数会让这个工作变得简单:QMetaMethod::fromSignal()
和QObject::isSignalConnected()
。
QMetaMethod::fromSignal()
新的 static 函数QMetaMethod::fromSignal()
将一个成员函数(一个 signal)作为参数,返回对应的QMetaMethod
。它可以很方便地用于新的connectNotify()
:
void MyClass::connectNotify(const QMetaMethod &signal) { if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) { // 连接到 mySignal ...
为了避免每次调用connectNotify()
的时候都要重新查找一个 signal,应该将QMetaMethod::fromSignal()
的返回值作为一个 static 变量存储起来。
另外一个fromSignal()
容易混淆的地方是在执行 signal 的 queued 调用时(QMetaObject::invokeMethod()
也可以这么做,但它是基于字符串的):
QMetaMethod::fromSignal(&MyClass::mySignal) .invoke(myObject, Qt::QueuedConnection /* 其他参数 ... */);
QObject::isSignalConnected()
新的函数QObject::isSignalConnected()
用于检查一个 signal 是否有 slot 连接。这是一个基于QMetaMethod
的,用于替代QObject::receivers()
的函数。
void MyClass::disconnectNotify(const QMetaMethod &signal) { if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) { // 有 slot 从 mySignal 断开连接 if (!isSignalConnected(signal)) { // 该 signal 没有连接,我们可以释放其资源 ...
另外,你可以使用这个函数用于避免一次 emit 许多 signal,例如,如果 emit 需要非常复杂的计算,那么这将严重影响到系统的性能。
QTBUG-4844: QObject::disconnectNotify() is not called when receiver is destroyed
这个 bug 现在依然存在。它是说,当接收者销毁的时候,disconnectNotify()
没有调用。这里我们需要看看如何避免这一问题。
为了实现所期望的行为,连接需要记住其 signal id。因为当 receiver 销毁时,我们无法再获取 signal id(如果显式调用disconnect()
函数则不会有这个问题)。这将给所有的连接都增加一个 int。如果你有更好的解决方案,不要忘记将修改意见提交给 Qt 5。
函数返回类型
在 Qt 5 之前的版本中,我们必须使用QMetaMethod::typeName()
判断一个函数的返回值类型,其格式是const char *
。如果返回值是 void,typeName()
返回空字符串,而不是像QMetaType::typeName()
那样返回字符串“void”。这种不兼容但是很多奇怪的地方,例如:
if (!method.typeName() || !*method.typeName() || !strcmp(method.typeName(), "void")){ // 返回值是 void ...
我们必须这么判断。
在 Qt 5 中,我们可以使用新的函数QMetaMethod::returnType()
,其返回值是一个 meta-type id:
if (method.returnType() == QMetaType::Void) { // 返回值是 void ...
在 Qt 4,你不能使用QMetaType::Void
区分 void 和未注册类型(它们都是整型 0)。Qt 5 中的 QMetaType::Void
就是 void,新的QMetaType::UnknownType
则用于指定一个未注册到 Qt 类型系统中的类型。
(这么做的后果是,如果你现在有代码是将类型 id 与QMetaType::Void
(或者整型 0)进行比较,那么我的建议是,在切换到 Qt 5 的时候,再三检查你的逻辑:是检查是不是 void,未知类型,还是两个都要?)
为了与QMetaType
保持一致,当返回值类型是 void 的时候,Qt 5 的QMetaMethod::typeName()
返回字符串“void”。现有代码,用typeName()
返回空字符串代表 void,必须要进行修改(将returnType()
与QMetaType::Void
进行比较)。
函数参数
类似QMetaMethod::returnType()
,新的QMetaMethod::parameterType()
函数返回参数类型的 meta-type id 。QMetaMethod::parameterCount()
返回参数个数。使用这两个函数就可以替代旧的QMetaMethod::parameterTypes()
,后者以字符串的形式返回所有参数类型(名字)。
如果在元对象定义时,类型已知(内建类型),类型 id 会直接嵌入到元对象数据中。对于另外的类型,获得 id 则会是基于字符串的查找,这也并不会比之前慢(对于内建类型则会快很多)。
结论
Qt 5 的元对象系统(以及元类型系统)有了一些小的变化。函数现在有更恰当更易于维护的语义,而不是普通的 C 字符串。与 Qt 4 源代码兼容最大程度的保持着(如果没有错误,就不要修改——当然,还是会有些问题,我们需要在编译时修正)。现在,Qt 5 元对象系统几乎完成。许多 Qt 模块都使用到其中的一些新特性,从而获得更清晰的代码、更快速的运行效率。希望那些对于元对象系统持批评态度的人可以偃旗息鼓了。
3 评论
对moc文件比较陌生,望博主多多写文章,以驱本人愚钝啊!博主只是渊博,忘不吝赐教,谢谢哈。
moc 文件只是一个临时文件,不需要了解太多的细节,也不推荐自己修改(因为每次运行 moc 后都会覆盖),主要用于实现反射机制,所以只要按照文档中的使用即可。
谢谢哈