Verdigris: Qt without moc

Verdigris is a header-only library that can be
used with Qt. It uses macros to create a QMetaObject that is binary compatible with Qt's own
QMetaObject without requiring moc.
In other words, you can use Verdigris macros in your Qt or QML application instead of some of the Qt
macros and then you do not need to run moc.

Introduction

CopperSpice is a fork of Qt 4. Its main raison d'être is
to get rid of moc because they consider it bad enough
(IMHO wrongly).
To do so, they replaced the user-friendly Qt macro with some less friendly macros.

However, CopperSpice is a whole fork of Qt, meaning they are maintaining the whole library. This
also means they are recreating an ecosystem from scratch.
If they had made it binary compatible, then they could have removed moc without the need to maintain the
full Qt library. This is what Verdigris is.

Another problem of CopperSpice compared to Qt is that it generates and registers the QMetaObject
at run-time when loading the application. Meanwhile, Verdigris uses constexpr to generate
the QMetaObject at compile time. For this reason, binaries using CopperSpice are much bigger
than binaries using Qt (moc or Vedrigris), and take also more time to load because of the massive
amount of relocations.

Previous work

Most of the ground work is based on the code I wrote already in my previous blog post: Can Qt's moc be replaced by C++ reflection?. In that blog post,
I was trying to see if reflection could help replace moc, while keeping the convenience of the current Qt macros.
The goal was was to influence source compatibility as little as possible.

CopperSpice decided to use different macros that are less convenient.
The macros of Verdigris are based or improved upon the CopperSpice ones.

The first difference of Verdigris is the W_OBJECT_IMPL macro that needs to be written in the .cpp file.
This is one of the few points for which Verdigris is less convenient than CopperSpice as they do not need
this macro.

In CopperSpice, you cannot define a slot inline in the class definition. You don't have this restriction
with Verdigris.(Update: I was told it is possible with CopperSpice by putting the body within the CS_SLOT_1 macro)

Both CopperSpice and Verdigirs can have templated QObject class or nested QObject.
Verdigris cannot, however, have function local QObjects (because of the static member
staticMetaObject) and local classes cannot have static members.

From an implementation point of view, CopperSpice macros use __LINE__
to build an unique identifier,
which means that two macros cannot be put on the same lines. So you can't declare several slots in a line
or declare properties or signals/slots from a macro. (which ironically is one of the "problems" they raised about Qt4's moc).
Verdigris's macros do not have this problem.

Benchmarks

KitchenSink

I made the KitchenSink example from CopperSpice compile both with CopperSpice, Qt 5 with moc
or with Verdigris
(patch).
This table show the amount in minutes:seconds taken by make -j1

Qt 5 (moc)

Verdigris

CopperSpice

Compilation time

1:57

1:26

16:43

Binary size

1.32 MB

1.36 MB

115 MB

I was surprised to see that Verdigris compiles faster than using moc.
The cost of compiling the generated code in a separate compilation unit is what makes it slower, and
including the generated file is a common way to speed up the compilation (which was not done in this case).
CopperSpice is probably so slow because each translation unit needs to re-generate the code that generates
the meta object for all the included objects (including the headers from CsCore, CsGui, ...).
Verdigris, however, moves most of the slow-to-compile code in a W_OBJECT_IMPL macro in the .cpp code
that is only parsed for the corresponding translation unit.
Still, the tests take a very long time to compile, that's because they
have many objects with lots of special methods/properties and we are probably hitting non-linear
algorithms within the compiler.

Library loading time

Any program that links to a C++ library has some overhead because of the relocations and the init section.
This benchmark simply links an almost empty program with the libraries, and compute
the time it takes to run.

CopperSpice(CsCore, CsGui)

Qt 4(QtCore, QtGui)

Qt 5(Qt5Core, Qt5Gui, Qt5Widgets)

56ms

16ms

17ms

Loading CopperSpice is much slower because all the MetaObjects needs to
be created and the huge amount of relocations.
Note: Since the program is empty and has no QObject on his own, neither moc nor Verdigris were used.
Qt5 and Qt4 themselves were compiled with moc as usual.

Signals/Slots run-time performance

I built the Qt benchmarks for testing connecting and emitting of Qt signals.
There is no difference between Verdigris or the normal Qt. As expected since
the QMetaObject structure is the same and the moc generated code is equivalent
to the templated code.

CopperSpice is faster for signal emission because they inlined everything including
QMetaObject::activate. Something we don't do in Qt because we want to maintain binary
compatibility and inlining everything means we can't change much of the data structures used until
the next major version of Qt. This contributes largely to the code size which is two order
of magnitude more than using Qt.

Implementations details

As said previously, most of the constexpr code was based on what I wrote for the previous research.
I had to replace std::tuple with another data structure because std::tuple
turned out to be way too slow to compile. The GNU's libstdc++ implementation of std::tuple
does about 16 template recursion per parameter only
to compute the noexpect clause of its copy constructor. Which is quite limiting when the default limit
of template recursion is 256 (so that means maximum 16 types in a tuple). There is also the fact that
the compiler seems to have operation of quadratic complexity depending on the amount of template parameter
or instantiation. I therefore made my own binary tree structure that can compile
in a reasonable time. Instead of having tuple<T1, T2, T3, T4, T5, ....> we have
Node<Node<Node<Leaf<T1>,Leaf<T2>>,Node<Leaf<T3>,Leaf<T4>>>, ...>

Nonetheless, the whole thing is some pretty heavy advanced template and macro tricks. While working on
it I found and reported or even fixed compiler bugs in clang [1], [2],
[3],
and GCC [1], [2],
[3]

Conclusions

In conclusion, as CopperSpice has already shown, this shows that moc is not strictly necessary
for Qt features. The trade-off is the simplicity of the macros. The alternative macros from CopperSpice
and Verdigris are less convenient, and force you to repeat yourself. Complex template code can also increase
compilation time even more than the overhead of moc.

On the other hand, we think the approach of CopperSpice was the wrong one. By forking Qt instead
of being compatible with it, they give up on the whole Qt ecosystem, and the small CopperSpice team
will never be able to keep up with the improvements that are made within Qt.
(CopperSpice is a fork of Qt 4, it is way behind what Qt 5 now has)

Verdigris is a header-only library consisting on two headers that can easily be imported in a project
to be used by anyone who has reasons not to use moc. You can get the files from the
Github repository.

Woboq is a software company that specializes in development and consulting around Qt and C++. Hire us!