What's New in QMetaType + QVariant

As you might know, Qt has a metatype system which provides run-time dynamic information about types. It enables storing your types in QVariant, queued connections in the signal slot system and is used throughout the QML engine. With the upcoming Qt 6.0 release, we used the chance to revisit its fundamentals and make use of the functionality that C++17 gives us. In the following, we examine those changes, and explain how they might affect your projects.

QMetaType knows your types even better now

In Qt 5, QMetaType contained the information necessary to default construct a type, to copy it and to destroy it. Moreover it knew how to save it to and load it from QDataStream and stored some flags to describe various properties of it (e.g. whether the type is trivial, an enumeration, etc.). Additionally, it would store the QMetaObject of the type if it had any, and a numeric id to identify the type as well as the types name.

Lastly, QMetaType contained functionality to compare objects of a certain (meta-)type, to print them with qDebug and to convert from one type to another. You had to use QMetaType::registerComparators() and the other static register functions in QMetaType to actually make use of that functionality, though. That would put pointers to those functions into corresponding registries, basically mappings from metatype-ids to function pointers.

With Qt 6, the first thing we did was to extend the information stored in QMetaType: Modern C++ is now almost 10 years old, so it was about time to store information about the move constructor in QMetaType. And to provide better support for overaligned types, we now also store the alignment requirements of your types. Moreover, we considered the registries to be a bit clunky. After all, why should we require you to call QMetaType::registerEqualsComparator() when we could already know this by simply looking at the type? So in Qt 6, QMetaType::registerEqualsComparator, QMetaType::registerComparators, qRegisterMetaTypeStreamOperators and QMetaType::registerDebugStreamOperator have been removed. The metatype system will instead know about those automatically. The outlier here is QMetaType::registerConverterFunction. As there is no way to know reliably which functions should be used for conversions, and we allow to register basically arbitrary conversions, that functionality stays the same as it was in Qt 5.

With those changes we could also unify the handling of Qt internal types and user registered types: This means that for instance QMetaType::compare works now with int:

#include <QMetaType>
#include <QDebug>

int main() {
  int i = 1;
  int j = 2;
  int result = 0;
  const bool ok = QMetaType::compare(&i, &j, QMetaType::Int, &result);
  if (ok) {
    // prints -1 as expected in Qt 6
    qDebug() << result;
  } else {
    // This would get printed in Qt 5
    qDebug() << "Cannot compare integer with QMetaType :-(";
  }
}

QMetaType knows your types at compile time

Thanks to various advancements in C++’s reflective capabilities, we can now get all the information we require from a type at compile time – including its name. If you are interested in how this is implemented, you should look at this excellent StackOverflow answer. In Qt we use a very similar approach, albeit with certain extensions and workarounds for older compilers. What’s even more interesting than the implementation though is what it means for you. First of all, instead of creating a QMetaType via either

QMetaType oldWay1 = QMetaType::fromName("KnownTypeName");

or

QMetaType oldWay2(knownTypeID);

it is now recommended that you create your QMetaTypes with

QMetaType newWay = QMetaType::fromType<MyType>();

if you know the type. The other methods still exist, and are useful when you do not know the type at compile time. However, fromType avoids one lookup from id/name to QMetaType at runtime. Note that since Qt 5.15 you could already use fromType, but there it would still do a lookup. Moreover you could not copy QMetaType, limiting its usefulness and making it more convenient to pass type ids around. However, in Qt 6, QMetaType is copyable.

You might now wonder what this means for Q_DECLARE_METATYPE and qRegisterMetaType. After all, do we really need them if we can create QMetaTypes at compile time?

Let’s look at an example first:

#include <QMetaType>
#include <QVariant>
#include <QDebug>

struct MyType {
  int i = 42;
  friend QDebug operator<<(QDebug dbg, MyType t) {
    QDebugStateSaver saver(dbg);
    dbg.nospace() << "MyType with i = " << t.i;
    return dbg;
  }
};

int main() {
  MyType myInstance;
  QVariant var = QVariant::fromValue(myInstance);
  qDebug() << var;
}

In Qt 5, this would lead to the following error message with gcc (+ a a few more warnings about failed instantiations):

/usr/include/qt/QtCore/qmetatype.h: In instantiation of 'constexpr
int qMetaTypeId() [with T = MyType]':
/usr/include/qt/QtCore/qvariant.h:371:37:   required from 'static QVariant
QVariant::fromValue(const T&) [with T = MyType]'
test.cpp:16:48:   required from here
/usr/include/qt/QtCore/qglobal.h:121:63: error: static assertion failed: Type is
not registered, please use the Q_DECLARE_METATYPE macro to make it known to Qt's
meta-object system
  121 | #  define Q_STATIC_ASSERT_X(Condition, Message) static_assert(bool(Condition), Message)
      |
^~~~~~~~~~~~~~~
/usr/include/qt/QtCore/qmetatype.h:1916:5: note: in expansion of macro 'Q_STATIC_ASSERT_X'
 1916 |     Q_STATIC_ASSERT_X(QMetaTypeId2<T>::Defined, "Type is not registered, please use the Q_DECLARE_METATYPE macro to make it known to Qt's meta-object system");

That’s not great, but at least it tells you that you need to use Q_DECLARE_METATYPE. However, with Qt 6, it will compile just fine and the executable will print QVariant(MyType, MyType with i = 42), as one would expect. And not only QVariant, but queued connections work too without an explicit Q_DECLARE_METATYPE.

Now, what about qRegisterMetaType? That one is unfortunately still needed – assuming you need name to type lookups. While a QMetaType object knows the name of the type it has been constructed from, the global name to metatype mapping only occurs once one either calls qRegisterMetaType. To illustrate:

struct Custom {};
const auto myMetaType = QMetaType::fromType<Custom>();

// At this point, we do not know that the name "Custom" maps to the type Custom
int id = QMetaType::type("Custom");
Q_ASSERT(id == QMetaType::UnknownType);

qRegisterMetaType<Custom>();
// from now on, the name -> type mapping works, too
id = QMetaType::type("Custom")
Q_ASSERT(id == myMetaType.id());

Having the name to type mappings available is still required if you use old style signal-slot-connections, or when using QMetaObject::invokeMethod.

QMetaType knows your properties’ and methods’ types

The ability of creating QMetaType at compile time also allows us to store the metatypes of a class’ properties in its QMetaObject. This change is mainly motivated by QML, where this change brings us enhanced performance and it the future hopefully a reduced memory consumption1. Unfortunately, this change puts a new requirement on the types used in property declarations: The type (or if it’s a pointer/reference, the pointed to type) needs to be complete when moc sees it. To illustrate the issue, consider the following example:

// example.h
#include <QObject>
struct S;

class MyClass : public QObject
{
  Q_OBJECT

  Q_PROPERTY(S* m_s MEMBER m_s);
  S *m_s = nullptr;

  public:
    MyClass(QObject *parent = nullptr) : QObject(parent) {}
};

In Qt 5, there wasn’t an issue with this. However, in Qt 6, you might get an error like

In file included from qt/qtbase/include/QtCore/qmetatype.h:1,
                 from qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qobject.h:54,
                 from qt/qtbase/include/QtCore/qobject.h:1,
                 from qt/qtbase/include/QtCore/QObject:1,
                 from example.h:1,
                 from moc_example.cpp:10:
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h: In instantiation of 'struct QtPrivate::IsPointerToTypeDerivedFromQObject<S*>':
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:1073:63:   required from 'struct QtPrivate::QMetaTypeTypeFlags<S*>'
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:2187:40:   required from 'QtPrivate::QMetaTypeInterface QtPrivate::QMetaTypeForType<S*>::metaType'
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:2309:16:   required from 'constexpr QtPrivate::QMetaTypeInterface* QtPrivate::qTryMetaTypeInterfaceForType() [with Unique = qt_meta_stringdata_MyClass_t; TypeCompletePair = QtPrivate::TypeAndForceComplete<S*, std::integral_constant<bool, true> >]'
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:2328:55:   required from 'QtPrivate::QMetaTypeInterface* const qt_incomplete_metaTypeArray [1]<qt_meta_stringdata_MyClass_t, QtPrivate::TypeAndForceComplete<S*, std::integral_constant<bool, true> > >'
moc_example.cpp:102:1:   required from here
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:766:23: error: invalid application of 'sizeof' to incomplete type 'S'
  766 |         static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
      |                       ^~~~~~~~~
make: *** [Makefile:882: moc_example.o] Error 1

Note the static assert which tells you that the type must be fully defined. This can be fixed in three different ways:

  1. Instead of forward declaring the class, simply include the header in which S is defined.
  2. As including additional headers can negatively affect build times, you can use the Q_MOC_INCLUDE macro instead. Then only moc will see the include. Simply use Q_MOC_INCLUDE("myheader.h") instead of #include "myheader.h"
  3. Alternatively you could include the moc generated file in your cpp file. This of course requires that the needed header is actually included there.2

Lastly, there are rare cases where you have an intentionally opaque pointer. In that case, you need to use Q_DECLARE_OPAQUE_POINTER is used.

This is certainly suboptimal, though in our experience properties with incomplete types are not that common. In addition, we’re currently investigating extending our tooling support to make at least the detecion of this issue automatic.

Similarly, we also try to create the metatypes for return types and parameters of methods known to the metaobject system (signals, slots and Q_INVOKABLE functions). This has the advantage of of avoiding a few name to type lookups in string based connections and inside the QML engine. However, we are aware that incomplete types are very common in methdos. Therefore, for methods we still have a fallback path, and method types are not required to be complete, so no changes are needed there. If we can, we store the metatype at compile time in the metaobject, but if not we will simply look it up at runtime. There’s one exception though: If you register your class with QML by using one of the declarative type registration macros (QML_ELEMENT and friends), we require even method types to be complete. In that case we assume that all metamethods which you expose are actually meant to be used in QML, and you therefore prefer to avoid any additional runtime type lookups (note that this does not affect the parent class’ metamethods).

QMetaType powers QVariant

After we reworked QMetaType, we could also clean up the internals of our venerable QVariant class. Before Qt 6, QVariant internally distinguished between user types and builtin Qt types, significantly complicating the class. QVariant also could only store values that were at most the size of the maximum of sizeof(void *) and sizeof(double) in its internal buffer. Anything else would be heap allocated. With Qt 6, anything else would include commonly used classes like QString (as QString is 3*sizeof(void *) big in Qt 6). So clearly we had to rework QVariant for Qt 6. And rework it we did! We managed to simplify its internal architecture, and made common use cases faster. This includes changing QVariant so that it now store types <= 3*sizeof(void *) in its SSO buffer. Besides allowing the continued storage of QStrings without additional allocations, this also makes it possible to store polymorphic PIMPL’d types like QImage3 in QVariant. This should prove beneficial for item models returning images in data().

We also introduced some behaviour changes in existing methods of QVariant. We are aware that silent behaviour changes are a common source of bugs, but deemed the current behaviour to be bugprone enough to warrant it. Here’s the list of what changed:

  • QVariant used to forward isNull() calls to its contained type – but only for a limited set of Qt’s own types. This has been changed, and isNull() now only returns true if the QVariant is empty or contains a nullptr.
  • QVariant’s operator== now uses QMetaType::equals for the comparison. This implies a behavioral change for some graphical types like QPixmap, QImage and QIcon that will never compare equal in Qt 6 (as they do not have a comparison operator). Moreover, floating point numbers inside a QVariant are now no longer compared via qFuzzyCompare, but instead use exact comparisons.

Another noteworthy change is that we removed QVariant’s constructor taking a QDataStream. Instead of constructing a QVariant holding a QDataStream (which would be in line with the other constructors), it instead would attempt to load a QVariant from the datastream. If you actually want this behaviour, use operator>> instead. Note also that QVariant::Type and its related methods have been deprecated in Qt 6 (but still exist). A replacement API working with QMetaType::Type has been added. This is useful as QVariant::type() can only return QVariant::UserType for user types, whereas the new QVariant::typeId() always returns the concrete metatype. QVariant::userType does the same (and did so already in Qt 5), but from its name it wasn’t apparent that it also works for builtin types.

Lastly, we added some new functionality to QVariant:

  • QVariant::compare(const Variant &lhs, const QVariant &rhs) can be used to compare two variants. It returns a std::optional<int>. If the values were incomparable (because the types are different, or because the type itself is not comparable) , std::nullopt is returned. Otherwise, an optional containing an int is returned. The number is negative f the contained value in lhs is smaller than the one in rhs, 0 if they are equal, and positive otherwise.
  • It’s now possible to construct an empty QVariant from a QMetaType (instead of passing in a QMetaType::Type, which would then be used to construct a QMetaType). For similar reasons, it’s possible to pass QMetaType to the convert function.
  • QVariant now supports storing overaligned types, thanks to QMetaType storing alignment information in Qt 6.

Conclusion and Outlook

The internals of Qt’s metatype system are a part of Qt which most users rarely interact with. Nevertheless, it’s at the heart of the framework, and used to implement more user-centric parts like QML, QVariant, QtDbus, Qt Remote Objects and ActiveQt. With the updates to it in Qt 6, we hope to have it serve us as well in the next decade as it did in the last.

Speaking of the next decade, you might wonder what the future holds in store for the metatype system. Besides our already mentioned plans to use it to enhance the QML engine, we also intend to improve the signal/slot connection logic. Both of those changes should not affect your code in any way, but simply improve performance and memory usage in a few places. In the farther future, we also certainly will monitor how C++ evolves, especially when it comes to static reflection and meta-classes. While we do not expect moc to go away anytime soon, we do consider replacing some of its functionality with C++ features once they become widely available.

Oh, and by the way, we have added one more piece of new functionality in Qt 6.0: QMetaContainer. What’s that you ask? Well, watch this space for another blogpost coming soon and you’ll know.


  1. In 5.15 and 6.0, the QML engine copies the information from the property metatypes into a custom datastructure, called PropertyCache. By having the property metatype available, we can already speed this up a bit, as we do not have to lookup the metatypes by name. In the upcoming releases, we want remove the PropertyCache completely, and instead reuise the metatypes from the metaobject.↩︎

  2. Doing this might also improve your buildtimes as a welcome side effect.↩︎

  3. Though that depends on a pending change to make QPaintDevice a bit smaller.↩︎


Blog Topics:

Comments