[Development] QProperty and library coding guide
Volker Hilsheimer
volker.hilsheimer at qt.io
Thu Jul 16 12:43:58 CEST 2020
> On 16 Jul 2020, at 11:19, Ulf Hermann <ulf.hermann at qt.io> wrote:
>
>> 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 available.
>
> 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.
>
>
> best,
> Ulf Hermann
Thanks Ulf. Very informative even for someone who has spent some time on this already!
I don’t think Thiago’s question whether/how we can add additional QProperty-properties to public Qt classes without breaking binary compatibility is answered yet, so here’s my take on it:
The various macros involved provide this. Each property is represented by an instance of a struct with no data members, but just methods that forward calls to the accessor, which in Qt is typically the d-pointer (where the QProperty itself lives as well).
For pre-C++20 (where it’s possible to have zero-size structs), and for compilers that don’t respect the [[no_unqiue_address]] attribute, all these struct-instances are put into a union. In that case, a class using QProperty will be larger (by the same amount no matter the number of properties) than the same class in Qt 5. With C+++ 20 and compilers that do respect [[no_unique_address]], the size and layout of these classes will be the same.
Volker
More information about the Development
mailing list