[Development] QProperty and library coding guide
ulf.hermann at qt.io
Thu Jul 16 11:19:22 CEST 2020
> There's a flurry of changes going in right now about using QProperty in
> QObject-derived classes. But before those begin being approved, I'd like to
> see QProperty added to our library coding guide.
Do you mean https://wiki.qt.io/Coding_Conventions ? I can certainly add
some paragraphs there. However, let me first give an introduction here,
and answer some of your question.
QProperty is the way to enable QML-style bindings in C++. It gives you a
powerful way of expressing relations between properties in a succinct
way. You can assign a binding functor directly to a QProperty. Any
QProperties the functor accesses are automatically recorded. Whenever
one of the recorded QProperties changes, the functor is (eventually)
re-evaluated and the original property is updated. You don't have to
remember to connect any signals. This is a big step forward as
connecting all relevant signals to all relevant slots and manually
re-evaluating everything that depends on any changes adds a lot of error
prone boiler plate to many projects.
You may have noticed the "eventually" above. If you connect a signal to
a slot, the evaluation mechanism is "eager": When the signal arrives,
the slot is executed. You may delay the signal a bit by queuing it, or
you may suppress subsequent signals of the same type by connecting them
as "unique", but the fundamentals stay the same. Let's say you have a
Rectangle class with width, height, and area members. The widthChanged()
and heightChanged() signals are connected to a slot that calculates the
area. If you change both width and height in short succession, the area
will be calculated twice. The intermediate value may be way off the mark
and violate some expectations elsewhere in your code.
QProperty, in contrast, evaluates lazily. That is, dependent properties
are evaluated when they are read. Say you again have a Rectangle class
with width, height, and area members, this time as QProperties. If you
change width and height without reading area in between, nothing will be
calculated. The area member is just marked "dirty". The next time the
area is read, it is calculated on the fly, and the value will meet your
expectations. We can also save a significant amount of CPU cycles this way.
QProperty can be exposed to the meta object system, and behaves just
like a getter/setter/signal property there. Code that is aware of
QProperty can, however, interact with such properties in a more
efficient way, using C++ bindings and lazy evaluation. This is what
QtQml does. I expect that other modules could be adapted to do the same.
However, this comes at a cost. In particular, we need to save a pointer
to possible bindings in QProperty. Compared to the pure value you'd
usually store if you follow the getter/setter/signal pattern, this adds
4 or 8 bytes to each property. For larger objects this doesn't matter
much, but indeed we have many int, bool, pointer, etc. properties.
In addition, each QProperty needs to store its value so that it can
properly determine whether it has changed when it's recalculated. This
could probably be worked around by assuming it always changes, but then
we would still have to save the bindings and dirty bits. You can,
however, have getter/setter/signal properties that don't store anything
at all but are always calculated on the fly.
Mind that connecting a signal to a slot does have a memory cost, too, as
the connection object needs to be stored. In contrast to QProperty and
bindings this is only for connections you manually create, though.
QProperty additionally has a fixed overhead.
On top of this, in places where we need to send signals due to
compatibility concerns, we do need to evaluate properties eagerly and
find out whether they change, undoing much of the benefits QProperty
offers. We cannot delay the sending of the signal until the property is
read, as any reading of the property is frequently triggered by the
signal itself. Therefore, we have to bite the bullet and re-evaluate any
binding as soon as the property is marked dirty. This is what
QNotifiedProperty does. However, if we have binding support in place for
those properties, we can eventually deprecate the signals, and maybe
remove them in Qt 7. In the long term we could therefore still reap the
benefits of QProperty for those cases.
Obviously, replacing getters and setters with Q(Notified)Property is not
binary compatible. Any publicly exposed property we want to change we
need to change in Qt 6.0, or wait until Qt 7. We do have source
compatibility wrappers for QProperty and QNotifiedProperty available for
both, data members in the private or public object. We may still be
lacking some details here and there, but we are certainly aiming for
full source compatibility with Qt5.
So, many of the questions by Thiago boil down to the following: Do we
want to pay the price for C++ binding support in our public API?
We can break this question down by repository and module, or by
characteristics of the properties in question, and we can decide on
whether we want new code to use QProperty over getter/setter/signal
properties or not. Mind, however, that bindings are not actually our own
invention. Data bindings are a cornerstone of most modern UI frameworks,
and Qt is actually late to the game. If we want Qt to stay relevant,
then it needs to offer the same kind of convenience and performance that
other frameworks offer. This would be an argument for converting all
existing properties, and paying the price, just to make the binding API
This being said, we should realize that QtWidgets have been invented in
the last century. They are built around signals and slots, and
converting all those connections into bindings is likely an amount of
work on a similar scale as rewriting all of QtWidgets from scratch.
Therefore, the assumption so far is that we won't convert QtWidgets, and
in particular not QWidget with its N+1 properties.
Rather, the focus is currently on classes in QtCore, QtNetwork, and
QtGui. Those generally only have a few properties, which limits the
memory overhead introduced by a conversion.
Finally, there is an alternative. We could provide a compatibility
wrapper that makes getter/setter/signal properties available to
bindings. A prototype can be seen here:
https://codereview.qt-project.org/c/qt/qtbase/+/301937 . With such a
thing, you would only pay a price for properties you want to use as
QProperty. In turn, the price is higher. Considering the strategic issue
of keeping Qt relevant, we should limit the use of such a solution to
places where we really cannot avoid it.
More information about the Development