c++qtqmlownershipqqmllistproperty

QML QQmlPropertyList - contained object lifetimes and 'memory rules'


I am finding it difficult to confirm the rules for object ownership of items inserted into QQmlPropertyList when it is setup as part of a QML component defined in C++ with

Q_CLASSINFO("DefaultProperty", "values")

DTech asked something similar in : QQmlListProperty - writable QList violates QML's memory management rules?, but he was worried about the consequences of deleting components defined in QML. I am more worried about the life cycle requirements of the objects inserted into the list in general.

Now from what I understand:

  1. If the children are created and inserted from QML their parents will be automatically set and the objects are managed by the QmlEngine. (parent should already be defined on append)

  2. If I insert something from the C++ side I need to manage the lifetime myself. (parent will be nullptr on append)

The QT docs for warn against 'violating QML's memory management rules.' in http://doc.qt.io/qt-5/qqmllistproperty.html but I can't find anything that actually states these in a clear, concise way.

Currently I have implemented a wrapper around a vector very similar to http://doc.qt.io/qt-5/qtqml-referenceexamples-properties-example.html

However I added two rules:

  1. Appending checks to see if the object being added has a nullptr parent and if so takes ownership.
  2. On clear if any object is owned by the current list parent, executes 'deleteLater' on the object and sets its parent to nullptr

I am assuming that because we set the parent when adding objects with nullptr parents, QT will delete the QObjects owned automatically when the parent goes out of scope.

Are there anymore more rules that I am missing and need to be aware of ?

Code for my implementation just in case the above requires a bit of clarification :

template <typename T>
class QmlListFacade {
 private:
  std::vector<T *> _storage;

  static void append(QQmlListProperty<T> *list, T *newValue) {
    // take ownership of the object if none are currently defined
    auto obj = static_cast<QObject *>(newValue);
    if( obj->parent() == nullptr) {
      obj->setParent(list->object);
    }
    auto internalList = reinterpret_cast<QmlListFacade *>(list->data);
    internalList->_storage.push_back(newValue);
  }

  static int count(QQmlListProperty<T> *list) {
    return reinterpret_cast<QmlListFacade *>(list->data)->_storage.size();
  }

  static T *get(QQmlListProperty<T> *list, int index) {
    return reinterpret_cast<QmlListFacade *>(list->data)->_storage[index];
  }

  static void clear(QQmlListProperty<T> *list) {
    auto internalList = reinterpret_cast<QmlListFacade *>(list->data);
    for( auto item :internalList->_storage){
      // only delete if we are the owners.
      auto obj = static_cast<QObject *>(item);
      if( obj->parent() == list->object){
         obj->setParent(nullptr);
         obj->deleteLater();
      }
    }
    return internalList->_storage.clear();
  }

 public:
  QmlListFacade() = default;


  QQmlListProperty<T> values(QObject *parent) {
    return QQmlListProperty<T>(parent, this, &QmlListFacade::append, &QmlListFacade::count, &QmlListFacade::get,
                               &QmlListFacade::clear);
  }

};


Solution

  • From the perspective of the one who discovered QML's iffy object lifetime management and struggled with this for almost 3 years now, in that window of experience I did not encounter any case of declaratively defined object trees being mishandled.

    The problems are with objects that are created dynamically, those tend to occasionally get deleted regardless of whether they have a parent, or how many active references to them exist in the code. If you want to ensure those don't get deleted, give them explicit CPP ownership.

    This will prevent the engine from destorying those objects while still in use. The objects will still be collected when their parents are destroyed, as that happens in the C++ API.

    Note that this warning is only featured in the constructor that takes in a QList and not the one that takes in the control function pointers. So if anything, you may at least find comfort in the fact you are not using that format. I've done plenty of testing on both and did not encounter any functional differences tho. That warning might have been misplaced, poorly worded and even entirely meaningless in addition to not providing any context of what it means and what its implications might be. Just be thorough at testing to catch any problems in their infancy.