QtCS2021 - Qt Property Bindings
Session Summary
One of the major changes in Qt 6 are the introduction of bindings for QProperties. This talk will outline why you should care about them and how to add them to a Qt 6 class. Finally we would like to welcome feedback on experiences you may already have made by using them.
Session Owners
- Andreas Buhr (andreas.buhr@qt.io)
- Sona Kurazyan (sona.kurazyan@qt.io)
- Fabian Kosmale (fabian.kosmale@qt.io)
- Ivan Solovev (ivan.solovev@qt.io)
Notes
Use cases
- Having declarative bindings in C++
- In Qt5 we had to use signal/slot connections to get updates when a property changes (via notify signals)
- With the new bindable properties we can still have notify signals, but it's not necessary anymore, you can bind to a property or subscribe to value changes
- QML/C++ integration
- There's now complete integration of QML binding system with the C++ binding system
- Usage with Q_GADGET, which doesn't allow signals
Performance considerations
- For QML currently performance is mostly about the same as for old QML bindings (due to a recent regression, now it's actually a bit slower, but will be fixed soon)
- C++ bindings are significantly faster, because all actual work is done in C++, which can be better optimized by the compiler
- The new bindings allows group updates, to avoid multiple intermediate re-computations of properties depending from each other
Current API
- Use Q_OBJECT_BINDABLE_PROPERTY/Q_COMPUTED_PROPERTY macros to declare bindable properties, and add bindablePropertyName() method to give access to binding functionality
- QObjectBindableProperty can be used for trivial getters and setters, to read/writes value, emit notify signal
- QObjectComputedProperty can be used for read-only properties without a storage for data
- There's also internal QObjectCompatProperty (can be declared via Q_OBJECT_COMPAT_PROPERTY macro) for more complicated cases (e.g. some additional logic required in the setter)
Binding evaluation
- Eager evaluation is used
- Lazy evaluation turned out impossible, since many parts of Qt are not meant for use with lazy evaluation (e.g. scene graph)
- The order of updates of multiple dependent properties is not defined
Limitations
- No cross-thread bindings possible, due to using a thread-local storage for tracking dependencies
- Public API supports only trivial getters/setters
- Computed properties can have custom getters, but the use cases are limited (because of being read-only)
- All the properties involved in bindings should be also bindable
- Objects used must outlive the bindings
- Binding code must not use co_wait
- Bindable value needs to have same type as the property
- Bindable properties aren't feasible for value classes: not clear what should happen with the binding when copying, especially in case of COW classes
- Virtual getters/setters are problematic
- virtual getters are possible only for Q_OBJECT_COMPUTED with limited use-cases
- virtual setters are possible only by using internal Q_OBJECT_COMPAT_PROPERTY, but overriding methods should always call the base implementation
- Properties with both custom getter and setter are problematic and should be handled on case by case basis (for example refactoring the code, or introducing additional properties). Each case needs to be evaluated, to decide if the additional complexity is worth it.
Road ahead
The main goal for the future work is to overcome the current limitations:
- Support custom getters in the Q_OBJECT_COMPAT property
- Make Q_OBJECT_COMPAT property a public API, to allow more complicated cases
- We have plans to improve our internal tooling
- Add test helpers for properties
- There are currently semi-private helpers, need to make ready for general use
- Add something similar to QSignalSpy to track the changes of bindable properties even without the signals
- Improve performance if possible
- Consider making binding evaluation smarter (e.g. do topological sort to avoid redundant updates)
Q&A
What kind of APIs/applications will benefit from the new property system (outside QML)?
Some code may be simplified, by creating the dependencies between properties. But this may cost some memory for dependency tracking.
How the grouping works?
Using Qt::beginPropertyUpdateGroup() and Qt::endPropertyUpdateGroup() functions before and after end of a transaction respectively.
These methods are thread-local: you can have bindings evaluating in different threads at the same time, you don't want a transaction in two different threads.
Proposal here: add a method to wrap the begin/endPropertyUpdateGroup(). Something like void runInUpdateGroup(std::function f);
How to decide when to convert classes to the new system?
- Currently it's up to the maintainers/developers
- We don't necessarily want to update all properties, depends on a use case
- We will re-evaluate what to port, once we have a public Q_OBJECT_COMPAT macro with a custom getter support
- Converting anything that we want to expose to QML is a good idea
- Anything that has a changed signal and can cause updates in other parts of application is a good candidate
- Also need to consider the required code complexity for porting, if it gets too complicated, doesn't make sense.
Any plans to fix the problems with access from multiple threads with external locking?
You cannot use cross-thread bindings, but reading a property from multiple threads is possible. But we still don't recommend doing it, because of some possible complications:
- You should lock not only the property you're accessing, but also all the properties involved in its evaluation. If it's possible to make sure all the related properties are locked, then it's OK.
- There could be issues if you don't have a recursive mutex, it might be very easy to accidentally lock the same mutex twice during the binding evaluation