原文地址:http://labs.qt.nokia.com/2012/06/22/changes-to-the-meta-object-system-in-qt-5/ 翻译:豆子

译文地址:http://www.devbean.net/2012/06/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,因为类似如下代码的隐式类型转换将会使程序崩溃,但是却不会有任何警告:

为了解决这个问题,我们在 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 内部):

 

文档说,你应该将 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():

 

为了避免每次调用connectNotify()的时候都要重新查找一个 signal,应该将QMetaMethod::fromSignal()的返回值作为一个 static 变量存储起来。

另外一个fromSignal()容易混淆的地方是在执行 signal 的 queued 调用时(QMetaObject::invokeMethod()也可以这么做,但它是基于字符串的):

 

QObject::isSignalConnected()

新的函数QObject::isSignalConnected()用于检查一个 signal 是否有 slot 连接。这是一个基于QMetaMethod的,用于替代QObject::receivers()的函数。

 

另外,你可以使用这个函数用于避免一次 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”。这种不兼容但是很多奇怪的地方,例如:

 

我们必须这么判断。

在 Qt 5 中,我们可以使用新的函数QMetaMethod::returnType(),其返回值是一个 meta-type id:

 

在 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 模块都使用到其中的一些新特性,从而获得更清晰的代码、更快速的运行效率。希望那些对于元对象系统持批评态度的人可以偃旗息鼓了。