QObject Lifetime & Ownership Guidelines

Overview

Qt’s object model is built around heap allocation + parent–child ownership. However, ManiVault’s codebase deliberately uses a different pattern in many places:

Most QObject-derived helper objects are stored as stack/member subobjects, while only top-level components are dynamically allocated.

This document explains: - When this pattern is safe - When it is unsafe - Why ManiVault uses it - The constraints developers must follow


Qt’s Default Ownership Model

In standard Qt practice:

auto *child = new QObject(parent);
  • Parent owns the child.

  • Parent deletes child in QObject::~QObject().

  • This model assumes heap allocation.

This is the most common and safest approach when: - Objects are reparented - Ownership is transferred - Objects are shared across components - Objects are passed to Qt containers (menus, widgets, toolbars, etc.)


ManiVault’s Allocation Strategy

Design Principle

In ManiVault:

  • Top-level components are heap allocated.

  • Internal helper QObjects are often stack/member objects.

  • Parent is typically set to this.

  • Ownership is strictly hierarchical and local.

Example:

class StandardItemModel : public QStandardItemModel {
    Q_OBJECT
private:
    gui::NumberOfRowsAction _numberOfRowsAction{this};
};

Why We Do This

The reasons are architectural and deliberate:

  1. Deterministic lifetime

    • Member subobjects are destroyed automatically.

    • No raw new/delete management.

  2. No scattered heap allocations

    • Improves locality.

    • Reduces pointer chasing.

    • Easier reasoning about ownership.

  3. Strict containment

    • Helpers never outlive their owner.

    • Ownership boundaries are explicit in class structure.

  4. Architectural clarity

    • Object graph mirrors class composition.

    • No hidden ownership transfers.

This approach works because the majority of these objects: - Are not reparented - Are not handed to ownership-taking APIs - Do not cross thread boundaries - Are fully contained within their owning component


When Stack-Allocated QObjects Are Safe

A QObject-derived member object is safe only if all of the following are true:

✔ Parent is either: - this (the enclosing owner), OR - nullptr permanently ✔ It is never reparented ✔ It is never passed to APIs that assume ownership Examples of risky APIs: - QWidget::addAction() - QMenu::addAction() - QToolBar::addAction() - Any API that may call setParent() ✔ It is never deleted via deleteLater() ✔ It does not receive queued signals after derived destruction begins


Common Failure Mode (Linux-only Crashes)

Typical assertion:

ASSERT failure in qobjectdefs_impl.h:
"Called object is not of the correct type
(class destructor may have already run)"

This usually indicates:

A queued signal is invoking a slot in a derived class after that derived destructor has already executed.

This is not inherently a stack-allocation issue.
It is usually caused by:

  • Qt::QueuedConnection

  • Cross-thread AutoConnection

  • Timers still running during teardown

  • Signals emitted during destruction

Linux timing makes these more visible than Windows.


Required Teardown Discipline

When using stack/member QObjects:

Always stop active emitters in destructor

~ActionsHierarchyModel() override
{
    _timer.stop();
    QObject::disconnect(&_numberOfRowsAction, nullptr, this, nullptr);
}

Avoid queued connections into derived classes during teardown

If necessary, disconnect in the derived destructor.


When Heap Allocation Is Required

Use heap allocation (new + parent) if any of the following apply:

  • Object may be reparented

  • Object may outlive the owning component

  • Object participates in UI container ownership

  • Object crosses thread boundaries

  • Object may receive queued events late in destruction

  • Object is part of a plugin boundary

  • Ownership is not strictly hierarchical

Example:

_numberOfRowsAction = new gui::NumberOfRowsAction(this);

This eliminates: - Accidental ownership transfer - Undefined delete behavior - Destructor ordering hazards


Project Policy

We do NOT mandate heap allocation for all QObjects.

Instead:

  • Stack/member QObjects are allowed and widely used.

  • They must follow the safety constraints defined above.

  • High-risk categories should prefer heap allocation.

High-risk categories

Prefer heap allocation for:

  • QAction

  • QTimer

  • UI-related QObjects

  • Cross-thread workers

  • Plugin-boundary objects


Decision Rule for Developers

Before adding a stack-allocated QObject member, ask:

“Could any other object ever delete or reparent this QObject?”

If the answer is yes or maybe, allocate it on the heap.

If the answer is definitively no, stack allocation is acceptable.


Summary

ManiVault’s stack-allocation pattern:

  • Is intentional.

  • Improves architectural clarity and determinism.

  • Works reliably under strict constraints.

  • Requires disciplined ownership and teardown management.

We do not refactor the entire codebase to heap allocation.

Instead, we:

  • Maintain strict containment rules.

  • Convert only high-risk objects to heap allocation.

  • Ensure teardown is safe against queued delivery.