Crash course in Qt for C++ developers, Part 2

alex  04 Sep, 2018
cpp  Qt  crash-course  meta-object-system 

In the last post we learnt about the event loop and how and why you should avoid blocking it. Another concept that you might have already stumbled upon is the meta-object system. If you have gone through the compile output of a Qt project you should have seen some traces of it; the output looks a bit different compared to compiling standard C++ code. If you haven't done it already, try to compile one of the Qt examples. See if you can spot something unusual about the output.

This is the second post in the series "Crash course in Qt for C++ developers" covering the Meta-object system (including QObject and MOC). The other topics are listed below.

  1. Events and the main event loop
  2. Meta-object system (including QObject and MOC)
  3. Signals and slots - communication between objects
  4. Hierarchy and memory management
  5. MVC or rather model/view and delegate programming
  6. Choose your camp Quick/QML-camp or Widgets-camp
  7. Qt Quick/QML example
  8. Qt Widgets example
  9. Tooling, e.g. Qt Creator
  10. Remaining good-to-know topics
  11. Where to go from here?

Let's go back to the Qt example. Did you really compile it? Did you see something like...?

Automatic MOC for target...

Qt includes some C++ extension compilers which automatically generate extra code and files before compilation and linking. The Meta-Object Compiler, or MOC for short, is a tool that parses all header files in the project. Depending on what's defined in them, the tool might (see Q_OBJECT below) generate a companion file for a class named moc_class-name.cpp. To enable the MOC step, have a look at the Qt documentation or if you're familiar with CMake: just add AUTOMOC ON as a property to the target.

Qt has been criticised by some developers for this extra compilation step. However, it's done for good reasons. If you're interested in why such a decision was made, Qt has composed an interesting article which has got you covered.

The generated moc_-files implement functions which are used for several different features. The perhaps most important ones are the signals and slots mechanism, the run-time type information (RTTI), and the dynamic property system.

The first feature, signals and slots, is the main reason for introducing the MOC system and is also a big subject on its own. Therefore, a whole post will be dedicated to it; look for the next post in the series. This post will cover the last two: run-time type information and the dynamic property system as well as a very special object called the QObject.

The man, the myth, the QObject

You might wonder what's so special about the QObject. In order to do any of the aforementioned concepts in Qt you must subclass a QObject and for signals and slots you'll also have to define the macro Q_OBJECT in the class. Actually, just to get a sense of how important the object is, all Qt identity objects inherit from the QObject. And there are many of them in the framework.

Although the Q_OBJECT is optional when subclassing a QObject, the official documentation recommends to always do it: without it, some functions may result in unexpected behaviour. The macro Q_OBJECT is what the MOC is looking for in order to generate the moc_-file. A typical example of a Qt class will look like this:

#include <QObject>

class MyClass : public QObject {
  Q_OBJECT
  Q_DISABLE_COPY(MyClass)
  // ... properties ...

public:
  MyClass();
  // ... functions and member variables ...

};

Actually, this is probably what most of your Qt classes will look like. By subclassing QObject we have now enabled the meta-object system and can now retrieve some runtime information about the class, such as the name of it:

MyClass myclass;
qDebug() << myclass.metaObject()->className();

qDebug() provides an output stream which can be used for debugging information, i.e. the code above will output:

MyClass

You might now think "This is nothing new! It is already supported in C++ using typeid(myclass).name()".

Yes, true! However, the Qt system doesn't require native RTTI compiler support. In addition, without a RTTI supported compiler, it's still possible to perform dynamic casts on QObjects using qobject_cast() instead of the traditional C++ dynamic_cast(). qobject_cast even works across dynamic library boundaries.

Did you see the macro Q_DISABLE_COPY(MyClass) defined in the class? It is, not surprisingly, used to prevent users from copying or moving the object. This is by design and I've written another blog post which is covering the reasons behind this: why qobject subclasses are not copyable.

Qt's sophisticated property system

In addition to RTTI, the class can now be extend with properties. Qt's property system is extremely flexible and works cross-platform without relying on any non-standard compiler features. In fact Qt's property system is so powerful that you can dynamically create properties during run-time, such as:

MyClass myclass;
myclass.setProperty("text","Hello world");
qDebug() << myclass.property("text").toString();

which will, as expected, output:

Hello world

There are several reasons why you'd want to extend your class with properties instead of using the standard member variables. I believe the main usage is to export them to the QML engine, which will be covered in a future post. To add properties to the class, the macro Q_PROPERTY is used, for example:

Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)

where the first argument is the type followed by the name of the property. The READ and WRITE accessors corresponds to getters and setters, whereas NOTIFY is optional and used to specify a signal (more on this in the next post). These are only some of the basic functionality that the property system provides, however it can be extended with many more features which you can read more about here.

Let's extend our class with a couple of properties:

class MyClass : public QObject {
  ...
  Q_PROPERTY(QString name READ name WRITE setName)
  Q_PROPERTY(QString creator READ creator WRITE setCreator)
  ...
public:
  ...
  const QString& name() const;
  void setName(QString name);

  const QString& creator() const;
  void setCreator(QString creator);

private:
  QString m_name{"DeLorean"};
  QString m_creator{"Dr. Emmett Brown"};
};

We've only added two getters and setters; each getter and setter pair for each property. It's now possible to, for example, loop through the properties by using the QMetaObject and print them:

 MyClass myclass;
 const QMetaObject* metaobject = myclass.metaObject();
 int count = metaobject->propertyCount();
 for (int i{0}; i < count; ++i) {
   QMetaProperty metaproperty = metaobject->property(i);
   const char* name = metaproperty.name();
   QVariant value = myclass.property(name);
   qDebug() << name << ": " << value.toString();
 }

which outputs:

name: "DeLorean"
creator: "Dr. Emmett Brown"

values are stored as QVariants. A QVariant is used as union for most Qt data types.

If you'd like to explore the full example code, feel free to fork the following GitHub project:

https://github.com/Fagrell/cleanqt.io-posts

and browse to /04-sep-2018/example.

QObject enables even more features

In this post we've covered MOC, Qt's RTTI, qobject_cast and the property system. These features are very impressive alone, however by using the meta-object system you'll gain many more. Here are some of them:

  • QObjects enable translations of strings for internationalisation using tr().
  • Remember the events that were covered in the previous post? All QObjects can receive and filter QEvents.
  • QObjects can be used with guarded pointers which are automatically set to 0 after deletion.
  • QObjects, Q_OBJECT and the MOC enable the perhaps most powerful feature of them all: the seamless inter-object communication signals and slots, which will be covered in the next post.

As a final point, I would like to mention that, although the QObject is extremely useful, the overhead of subclassing a QObject is rather significant. It should probably be avoided if you're not going to use any of its features. Plain data structures shouldn't also inherit from QObjects since they then won't be copyable or movable.

See you next time!

Share the post

Latest