原文地址:Kent Hansen - Changes to the Meta-Object System in Qt 5
Qt 5的元对象系统作出了一些改变,既有底层变化,又有API的变化。其中有些修改与Qt 4不是源代码兼容的。本文将介绍这些改变,以及如何修改现有代码,使其能够使用Qt 5进行编译。同时,我们也将阐述下新增加的一些 API,使QMetaMethod更方便使用。
元对象数据(例如由moc生成的)有一个版本号,用于描述元对象的内容(格式/布局)和特性。当我们向新的主要版本的Qt的元对象增加新特性时(例如让一个类的构造函数支持内省(introspect)),我们必须更新这个元数据版本号。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()是基于模板的,将函数的完整签名作为字符串存储就不那么明智了。(QObject::connectNotify()函数造成Qt4内部不得不处理签名字符串,我们会在后文详细说明。)
函数内省(introspect)的另外一个常见用例是动态绑定(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()的新函数,在某些情况下仍然需要获取完整的签名,比如在调试的时候。
QObject::connectNotify()和disconnectNotify()是两个很少被重写的虚函数。这两个函数适用在别人连接到你的信号或断开连接的时候。一个潜在的使用情景是,实现隐藏代理(lazy proxy)的时候,将一个内部对象(后端)连接到一个公有的信号上面。例如qtsystems模块就大量使用了这一特性。
在Qt 5之前,connectNotify()为了配合基于字符串的QObject::connect()函数,其参数是const char指针,指向标识所连接的信号的一般化签名,用于识别连接到的是哪一个信号。对于基于模板的QObject::connect()以及诸如QML 或QtScript中使用的自定义的连接,只为了调用一个通常没有被重写的虚函数,就让Qt准备一个以字符串形式表达的信号, 十分不明智。
另外,基于字符指针的connectNotify()不是Qt的风格。即便你能够拼写正确每一个函数签名,你也有可能陷入下面的陷阱(这样的问题代码甚至出现在Qt内部):
void MyClass::connectNotify(const char *signal)
{
if (signal == SIGNAL(mySignal())) {
// 这永远不会执行,因为比较的是指针而不是字符串内容
文档说,你应该将signal参数包装成QLatin1String,但经常会忘记这么做。Qt 5的解决方案是不允许出现这样的错误。
在Qt 5中,QObject::connectNotify()和disconnectNotify()接受一个QMetaMethod,而不是字符指针。QMetaMethod仅仅是QMetaObject指针和一个索引的轻量级封装。这样就不会被建立连接的方式所左右。
这种变化同时允许我们将对connectNotify()和disconnectNotify()的调用转移到内部基于索引的connect()和disconnect()函数(QMetaObject::connect(),该函数在Qt内部有好几处使用)的实现中。在实际应用中,这意味着即使QObject::connect()没有显式调用(例如connectSlotsByName())你重新实现的connectNotify()函数也会按预期被调用。最后,我们可以丢弃掉某些Qt里的蹩脚的代码,例如那些手工调用的connectNotify(const char *),转而使用基于索引的connect()。
已经重写了connectNotify()和disconnectNotify()的现有代码需要迁移到新的API。有两个函数会让这个工作变得简单:QMetaMethod::fromSignal()和QObject::isSignalConnected()。
新的静态函数QMetaMethod::fromSignal()将一个成员函数(一个信号)作为参数,返回对应的QMetaMethod。它可以很方便地用于新的connectNotify():
void MyClass::connectNotify(const QMetaMethod &signal)
{
if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) {
// 连接到mySignal ...
为了避免每次调用connectNotify()的时候都要重新查找一个信号,应该将QMetaMethod::fromSignal()的返回值作为一个静态变量存储起来。
另外fromSignal()的一个鲜为人知的用法是用于排队发射信号(queued emission)(QMetaObject::invokeMethod()也可以实现相同的功能,但它是基于字符串的):
QMetaMethod::fromSignal(&MyClass::mySignal)
.invoke(myObject, Qt::QueuedConnection /* 其他参数 ... */);
新的函数QObject::isSignalConnected()用于检查一个信号是否有槽与之连接。这是一个基于QMetaMethod的,用于替代QObject::receivers()的函数。
void MyClass::disconnectNotify(const QMetaMethod &signal)
{
if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) {
// 有槽从mySignal断开连接
if (!isSignalConnected(signal)) {
// 该信号没有连接,我们可以释放其资源 ...
另外,你可以使用这个函数避免将许多信号一同发射,例如,如果信号的发射将引起可能严重影响程序性能的复杂计算。
更新:https://codereview.qt-project.org/#change,29423已经修正这个问题
这个bug现在依然存在。这是造成disconnectNotify()“不完整”(或“不工作”,取决于你如何看待)的缺陷,所以我很希望该问题被修正。
为了能有效地实现所期望的行为,连接需要记住信号的id。因为当接收者被销毁时,我们无法像通常在(显式)断开连接时那样再获取信号id。这将给所有的连接都增加额外字节支出(一个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(),后者以字符串的形式返回所有参数类型(名字)。
如果在元对象定义时(如调用moc时),类型已知(内建类型),类型id会直接嵌入到元对象数据中, 查找将非常迅速。对于其他类型,id的解析则会是基于字符串的查找,这也并不会比之前慢(还可以通过缓存解析结果进行优化)。
Qt 5的元对象系统(元类型系统也是如此,但细节留到下一篇博文)有了一些小的变化。函数现在有更恰当更易于维护的语义,而不是普通的C字符串。与Qt 4源代码兼容最大程度的保持着(如果兼容性没有破坏,请不要修改——但有些Qt 4的错误我们不得不解决)。我们清除了实现代码中的遗留问题。 一些Qt模块已经使用了新特性,从而获得更清晰、快速的代码。希望那些对于元对象系统持批评态度的人可以偃旗息鼓了。
(译者注:感谢Bai Jing对本篇博客全文进行了修正。)