Memory management for list of QObject* results in "Cannot read property X of null" errors
-
wrote on 2 Mar 2022, 10:01 last edited by
I need to create dynamic list of
QObject*
(representin a custom model) and expose them to QML. The problem is that QML tries to re-use previously deletedQObject*
which ends up with errors at runtime:qrc:/MyWidget.qml:6: TypeError: Cannot read property 'value' of null
Here is my model:
#include <QObject> class SubModel : public QObject { Q_OBJECT public: SubModel(int value) : m_value(value) {} Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) int value() { return m_value; } void setValue(int value) { m_value = value; emit valueChanged(); } signals: void valueChanged(); private: int m_value; };
Here is the
QAbstractListModel
containing theSubModel
list (note thecreateSubModels()
method):#include <QAbstractListModel> #include <QObject> #include <QVariant> #include <memory> #include <vector> class ModelList : public QAbstractListModel { Q_OBJECT public: enum ModelRole { SubModelRole = Qt::UserRole }; Q_ENUM(ModelRole) void setSubModels(std::vector<std::unique_ptr<SubModel>> subModels) { beginResetModel(); m_subModels = std::move(subModels); endResetModel(); } int rowCount(const QModelIndex& parent = QModelIndex()) const override { return m_subModels.size(); } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { if (!index.isValid()) { return {}; } switch (role) { case ModelRole::SubModelRole: return QVariant::fromValue<SubModel*>(m_subModels[index.row()].get()); } return {}; } bool setData(const QModelIndex& index, const QVariant& value, int role) override { Q_UNUSED(index); Q_UNUSED(value); Q_UNUSED(role); return false; } QHash<int, QByteArray> roleNames() const override { QHash<int, QByteArray> roles; roles[ModelRole::SubModelRole] = "submodel"; return roles; } Q_INVOKABLE void createSubModels() { std::vector<std::unique_ptr<SubModel>> subModels; for (int i = 0; i < rand() % 5 + 1; i++) { subModels.push_back(std::make_unique<SubModel>(rand() % 100)); } setSubModels(std::move(subModels)); } private: std::vector<std::unique_ptr<SubModel>> m_subModels; };
Here is my QML widget using the
SubModel
instance (note the typedproperty
):import QtQuick 2.12 import MyLib.SubModel 1.0 Text { property SubModel subModel; text: subModel.value }
Here is the
main.qml
file:import QtQuick 2.5 import QtQuick.Window 2.2 import QtQuick.Controls 2.15 Window { visible: true Column { Button { text: "Create list" onClicked: modelList.createSubModels(); } Column { Repeater { model: modelList MyWidget { subModel: model.modelData } } } } }
And finally, here is the
main.cpp
file:#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include <iostream> #include "models.h" int main(int argc, char* argv[]) { qmlRegisterUncreatableType<SubModel>( "MyLib.SubModel", 1, 0, "SubModel", "This type can't be created in QML"); auto modelList = std::make_unique<ModelList>(); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; QQmlContext* rootContext = engine.rootContext(); rootContext->setContextProperty("modelList", modelList.get()); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
The firs time the
"Create list"
button is clicked it works fine. But the second time, the initialSubModule
pointers are deleted and QML tries to accessvalue
attribute ofnullptr
, resulting in warning. Thenm_subModels
is replaced and the GUI is effectively updated. I don't understand why QML tries to access model while I emitted thebeginResetModel()
signal.I know that I could change
ModelList
and makedata()
returns the actualint
value instead of aSubModel
pointer but this is not acceptable because I actually have several submodels in real code (separation of concerns).Solutions I thought of:
- Checking for
if submodel !== null
in QML but this doesn't look good - Replacing
std::unique_ptr
withnew
but this creates a memory leak - Using raw pointer with a Qt
parent
but memory will never be freed until exit - Using
QSharedPointer
withdeleteLater()
in destructor but this does not work all the time - Using
QSharedPointer
withsetObjectOwnership(JavaScriptOwnership)
in destructor but this seems hacky (not tested) - Using a
QList<QObject*>
instead ofQAbstractListModel
but this generated the same errors - Using a
QQmlListProperty
instead ofQAbstractListModel
but this generated the same errors - Re-using existing
SubModel*
instead of re-creating the list but this seems very convoluted
I have spent several days on this problem and I can't find a suitable solution. Would you have an idea please to keep my "SubModel" as it is and avoid QML warnings when the list is re-created?
- Checking for
1/1