Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to separate `QObject`'s parent-child relationship's concerns, e.g. to interoperate with other owners?

Consider this use case: When a QObject's lifetime is managed elsewhere, e.g. by the C++ scoped lifetime (as a local variable, or as a class member, etc.), or with a shared pointer, its parent should not attempt to delete it in the ~QObject() destructor. Is there a way to truly pass the ownership of the object to the shared pointer, so that the parent won't attempt to delete it?

What may be the reasons for setting a parent if we don't intend ownership? That they exist follows from the fact that the parent-child relationship is appropriated for multiple purposes in Qt, and there's no built-in way to decouple them:

  1. [GC] The parent acts as a garbage collector for the children objects: if any survive until parent's destruction, the parent will destroy and deallocate them.

  2. [Thread] The child's thread affinity follows that of the parent.

    This was added to Qt 4 when QObject began supporting multi-threaded operation and gained the thread property.

  3. [WidgetTree] Widgets use the parent-child relationship as an edge in the widget tree.

    • Except when the widget has the Qt::Window flag - then it is a top-level widget, and is a root of its own widget tree, but is still garbage-collected by the parent.

The immediate goal is to decouple the [GC] from other functionalities, and allow it to be disabled per-object. The extended goal is to decouple all three functionalities from each other.

like image 290
Kuba hasn't forgotten Monica Avatar asked Oct 27 '25 06:10

Kuba hasn't forgotten Monica


1 Answers

[GC]

We can intercept the parent's ~QObject() destructor and remove the object from the child list before the child objects in that list get deleted. Luckily, ~QObject emits the destroyed signal before deleting its children.

Thus, to change an object's parent to a non-owning parent, we must take action only when it matters: when the parents' destructor gets invoked. Thus, we

  1. Intercept parent's destroyed signal and clear the object's parent.

  2. Intercept the parent losing the child, or, if the parent doesn't receive such events, intercept the events sent to the child and use them as a blunt hook to detect if the parent got changed.

This can be implemented in a free-standing utility function and a helper class, without the need to modify the code of any of the objects involved.

// https://github.com/KubaO/stackoverflown/tree/master/questions/qobject-sep-concerns-55046944
#include <QtWidgets>

class ParentTracker : public QObject {
   QMetaObject::Connection connection;
   QObject *subjectParent = nullptr;
   inline QObject *subject() const { return parent(); }
   bool eventFilter(QObject *receiver, QEvent *event) override {
      qDebug() << receiver << event->type();
      if (receiver == subject()) {
         // Track parent changes on the child
         if (subject()->parent() != subjectParent) {
            detachFromParent();
            attachToParent();
         }
      } else if (event->type() == QEvent::ChildRemoved) {
         // Track child changes on the parent
         Q_ASSERT(receiver == subjectParent);
         auto *ev = static_cast<QChildEvent *>(event);
         if (ev->child() == subject()) {
            detachFromParent();
         }
      }
      return false;
   }
   void lostParent() {
      subject()->setParent(nullptr);
      detachFromParent();
   }
   void detachFromParent() {
      if (subjectParent) {
         disconnect(connection);
         connection = {};  // free the connection handle immediately
         subjectParent->removeEventFilter(this);
         subjectParent = nullptr;
      }
   }
   void attachToParent() {
      Q_ASSERT(!subjectParent);
      subjectParent = subject()->parent();
      bool snoopChild = !subjectParent;
      {
         auto *widget = qobject_cast<QWidget *>(subject());
         snoopChild = snoopChild ||
                      (widget && widget->testAttribute(Qt::WA_NoChildEventsForParent));
      }

      if (subjectParent) {
         auto *widget = qobject_cast<QWidget *>(subjectParent);
         snoopChild = snoopChild ||
                      (widget && widget->testAttribute(Qt::WA_NoChildEventsFromChildren));
         connection = connect(subjectParent, &QObject::destroyed, this,
                              &ParentTracker::lostParent);
      }
      if (snoopChild)
         subject()->installEventFilter(this);
      else {
         Q_ASSERT(subjectParent);
         subject()->removeEventFilter(this);
         subjectParent->installEventFilter(this);
      }
   }

  public:
   explicit ParentTracker(QObject *child) : QObject(child) {
      Q_ASSERT(subject());
      attachToParent();
   }
};

ParentTracker *detachQObjectOwnership(QObject *child) {
   Q_ASSERT(child && (!child->thread() || child->thread() == QThread::currentThread()));
   QObject *parent = child->parent();
   if (!parent) return nullptr;
   if (parent->thread() != child->thread()) return nullptr;
   return new ParentTracker(child);
}

template <class T> void setup(QPointer<QObject> &parent, QPointer<QObject> &child) {
   parent = new T;
   child = new T(static_cast<T*>(parent.data()));
   parent->setObjectName("parent");
   child->setObjectName("child");
   Q_ASSERT(parent && child);
}

int main(int argc, char *argv[]) {
   QApplication app(argc, argv);
   QPointer<QObject> parent, child, tracker;

   // parent-child ownership
   setup<QObject>(parent, child);
   delete parent;
   Q_ASSERT(!parent && !child);

   // parent-child without ownership
   setup<QObject>(parent, child);
   tracker = detachQObjectOwnership(child);
   delete parent;
   Q_ASSERT(!parent && child && tracker);
   delete child;
   Q_ASSERT(!parent && !child && !tracker);
}
like image 185
Kuba hasn't forgotten Monica Avatar answered Oct 28 '25 23:10

Kuba hasn't forgotten Monica



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!