七月 11, 2012
Comments
原文地址: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对本篇博客全文进行了修正。)
Download the latest release here: www.qt.io/download
Qt 6.11 is now available, with new features and improvements for application developers and device creators.
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.