Changes to the Meta-Object System in Qt 5

There are changes to the meta object-system in Qt 5, both under the hood and on the API side. Some of the changes are source incompatible with Qt 4. In this blog I'll describe those changes and how to adapt existing code. I'll also show a few small API additions that make it easier to work with QMetaMethods.

Removing support for Qt 4 meta-object versions

The meta-object data (e.g., as created by moc) has a version number that describes the contents (format/layout) and features of the meta-object. Whenever we add a new feature to the meta-object (such as making a class's constructors introspectable) in a new Qt (minor) release, we must also increase the meta-object version. The "last generation" Qt 4 meta-objects have version 6.

To maintain backward compatibility, new minor versions of Qt have to support the meta-objects of previous versions. That's achieved by checking the version of the individual meta-object at relevant places inside Qt, and falling back to an alternate codepath if the meta-object is of old age.

Since Qt 5 breaks binary compatibility with Qt 4 anyway, we took the opportunity to get rid of the support for old meta-object versions. The fallbacks would effectively be dead code in Qt 5, since Qt would never create a meta-object with a "Qt 4" version. Qt 5 meta-objects currently have version 7.

moc output from Qt 4 does not compile with Qt 5. In (the hopefully rare) case your codebase contains some hand-edited moc output (which Qt itself does in a few places, to implement "special" meta-objects), you'll have to recreate the moc output.

The various runtime meta-object builders in Qt (QMetaObjectBuilder, QtDBus, ActiveQt) have also been updated to generate version 7 meta-objects; since these are internal, existing code shouldn't be affected. (Hopefully nobody out there wrote their own meta-object builder... :) )

Methods are no longer identified by strings

Prior to Qt 5, the meta-object contained the full, normalized signatures of the class's signals, slots and Q_INVOKABLE methods, stored verbatim as zero-terminated strings. This probably made sense back when QObject::connect() was fully string-based (using the SIGNAL() and SLOT() macros). But now that there's a template-based QObject::connect(), storing the full signature is a less obvious choice. (Dealing with signature strings internally was always necessary in Qt 4 due to the QObject::connectNotify() function; more on that in the next section.)

Another common use case of method introspection is dynamic bindings (QML, QtScript), where the normalized signature is, again, not an ideal representation to work with. The implementation would be simpler and more efficient if it had direct access to a method's name and parameter types (no runtime parsing of strings required).

Lo and behold: In Qt 5, QMetaMethod has new functions name(), parameterCount(), and parameterType(int index). The full method signature is no longer stored verbatim in the meta-object data (only the method name is), since a compatible signature can be constructed on demand from the other bits of information.

Unfortunately, the existing QMetaMethod::signature() function returns a const char pointer; this leaked the fact that the signature used to be stored verbatim. We couldn't simply change the return type to QByteArray, since implicit casting would cause existing code like the following to break without warning:

// If signature() returns a QByteArray, it would go out of scope (and be destroyed) after this statement:
const char *sig = someMethod.signature();
// Do something with sig and watch bad stuff happen, probably ...

To resolve this, we've introduced a new function in Qt 5, QMetaMethod::methodSignature(). The old QMetaMethod::signature() function is no longer available; calls to it will trigger a compiler error stating that signature() has been renamed. Existing code should be ported to use QMetaMethod::methodSignature(). Even with new functions like QMetaMethod::name(), obtaining the full method signature can still be useful in some cases, e.g. for debugging.

connectNotify() and disconnectNotify()

QObject::connectNotify() and disconnectNotify() are two rarely-reimplemented virtual functions that you can use to do something when someone connects to or disconnects from one of your class's signals. One potential use is to implement lazy proxy (relaying) connections from some internal ("back-end") object to the public signals. The qtsystems module does that heavily, for example.

Prior to Qt 5, connectNotify() was biased towards the string-based QObject::connect(), in that it received a const char pointer to a normalized signature that identified the signal that was connected to. For the template-based QObject::connect(), and for custom connections done in e.g. QML or QtScript, making Qt prepare a string representation of the signal just to call a virtual function that's usually not reimplemented is sub-optimal.

Furthermore, the char pointer-based connectNotify() feels ... un-Qt-ish. Even if you managed to spell the normalized form of the signature, you could still fall into the following trap (it happened inside Qt, even):

void MyClass::connectNotify(const char *signal)
{
if (signal == SIGNAL(mySignal())) {
// This will never get reached; comparing pointers, not strings ...

The documentation says you should wrap the signal argument in a QLatin1String, but it's easy to forget to do so. The solution is to provide an API that doesn't allow for such mistakes.

In Qt 5, QObject::connectNotify() and disconnectNotify() receive a QMetaMethod instead of a char pointer. A QMetaMethod is just a thin wrapper around a QMetaObject pointer and an index. It's not biased towards any particular way a connection is established.

This change also allowed us to move the calls to connectNotify() and disconnectNotify() to the implementation of the internal index-based connect() and disconnect() functions (QMetaObject::connect(), which is used several places in Qt). In practice, this means that your connectNotify() reimplementation will get called as you would expect, even if QObject::connect() isn't called explicitly (connectSlotsByName(), for example). Finally, we could get rid of some blush-inducing code in Qt that manually called connectNotify(const char *) in conjunction with the index-based connect().

Existing code that reimplements connectNotify() and disconnectNotify() needs to be ported to the new API. There are two new functions that should make it relatively easy: QMetaMethod::fromSignal() and QObject::isSignalConnected().

QMetaMethod::fromSignal()

The new static function QMetaMethod::fromSignal() takes a member function (a signal) as argument and returns the corresponding QMetaMethod. It nicely complements the new connectNotify():

void MyClass::connectNotify(const QMetaMethod &signal)
{
if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) {
// Someone connected to mySignal ...

To avoid doing a specific signal lookup every time connectNotify() is called, you could keep the result of QMetaMethod::fromSignal() in a static variable.

Another obscure usage of fromSignal() is to perform a queued emission of a signal (QMetaObject::invokeMethod() would also do the trick, but it's string-based):

QMetaMethod::fromSignal(&MyClass::mySignal)
.invoke(myObject, Qt::QueuedConnection /* and arguments, if any ... */);

QObject::isSignalConnected()

The new function QObject::isSignalConnected() can be used to check whether anything is connected to a signal. It's a QMetaMethod-based alternative to QObject::receivers().

void MyClass::disconnectNotify(const QMetaMethod &signal)
{
if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) {
// Someone disconnected from mySignal
if (!isSignalConnected(signal)) {
// No more connections to this signal; we can release some resources now, perhaps ...

Alternatively, you can use this function to avoid emitting a signal altogether, e.g. if the emit would require some complex computations that could notably affect application performance.

QTBUG-4844: QObject::disconnectNotify() is not called when receiver is destroyed

UPDATE: Fixed by https://codereview.qt-project.org/#change,29423

This bug is currently still present, unfortunately. It's a deficiency that effectively renders disconnectNotify() "incomplete" (or "broken", depending on how you see it), so I'd really like to see the issue resolved.

To be able to implement the desired behavior efficiently, the connections would have to remember the signal id, since, when the receiver is destroyed, we cannot derive the signal id as we can in a normal (explicit) disconnect. This would add size overhead (one int) for all connections. If you can come up with a clever solution, there's still time to get it into 5.0 (it's likely too big of a behavior change to fix in a future minor release).

Method return type

Prior to Qt 5, you had to use QMetaMethod::typeName() to determine a method's return type, in const char pointer form. Quirkily, typeName() returned an empty string if the return type was void -- not the string "void", as QMetaType::typeName() does. This inconsistency led to confusion; I've seen code that does

if (!method.typeName() || !*method.typeName() || !strcmp(method.typeName(), "void")) {
// The return type is void ...

... just to be sure.

In Qt 5, you can use the new function QMetaMethod::returnType() instead, which returns a meta-type id:

if (method.returnType() == QMetaType::Void) {
// The return type is void ...

In Qt 4, you couldn't use QMetaType::Void to distinguish between void and unregistered types (they were both represented as the integer 0). In Qt 5, QMetaType::Void actually means the type void; the new value QMetaType::UnknownType is used to specify a type that isn't registered with Qt's type system.

(As an aside: If you have existing code that compares a type id to QMetaType::Void (or to the integer 0), I recommend double-checking your logic when switching to Qt 5 ("should this be checking for void, unknown types, or both?").)

For consistency with QMetaType, QMetaMethod::typeName() in Qt 5 returns the string "void" if the return type is void. Existing code that assumes that an empty typeName() string means void will have to be adapted accordingly (compare the returnType() to QMetaType::Void instead).

Method parameters

Similar to QMetaMethod::returnType(), the new function QMetaMethod::parameterType() returns the meta-type id of a parameter. QMetaMethod::parameterCount() returns the number of parameters. Consider using those two functions instead of the old QMetaMethod::parameterTypes() function, which returns the types (names) as a list of strings.

If the type is known at meta-object definition time (a built-in type, in the moc case), the type id will be embedded directly in the meta-object data, resulting in very fast lookup. For other types, the id resolution will fall back to string-based lookup, which is no slower than before. (It could be improved by caching the resolution somehow.)

Conclusion

For Qt 5, the meta-object system (and the meta-type system, too, but let's save the details for another post) has gone through a small metamorphosis. Methods are now represented and manipulated with proper semantics, not as plain (C) strings. Source compatibility with Qt 4 was largely kept (if it ain't broke, don't fix it -- but a few things were broke in Qt 4, and we were compelled to fix them). We cleaned out some legacy stuff in the implementation. Several Qt modules already use the new features, resulting in cleaner, faster code. Surely even the most staunch meta-critic will be appeased.


Blog Topics:

Comments