Qt Object Model & QObject

QObject hierarchy, parent-child memory management, properties, and the Qt meta-object system.

7 min read
47710 chars

What is QObject?

QObject is the base class of almost all Qt classes. It provides:

  • Signals & Slots — event-driven communication
  • Parent-child memory management — automatic deletion of children
  • Properties — runtime-accessible attributes via Q_PROPERTY
  • Meta-object system — runtime type information (RTTI)
  • Event handlingevent() virtual method

Parent-Child Memory Management

When a QObject has a parent, the parent takes ownership and deletes children automatically when it is destroyed. This prevents memory leaks without needing delete everywhere.

#include <QObject>
#include <QDebug>

class Device : public QObject {
    Q_OBJECT
public:
    explicit Device(const QString &name, QObject *parent = nullptr)
        : QObject(parent), m_name(name)
    {
        qDebug() << "Device created:" << m_name;
    }

    ~Device() {
        qDebug() << "Device destroyed:" << m_name;
    }

private:
    QString m_name;
};

int main() {
    QObject root;                              // parent
    Device *uart = new Device("UART", &root); // child of root
    Device *spi  = new Device("SPI",  &root); // child of root

    // When root goes out of scope, uart and spi are deleted automatically
    return 0;
}

Output:

Device created: UART
Device created: SPI
Device destroyed: SPI
Device destroyed: UART
Device destroyed: (root)

The Q_OBJECT Macro

Every class that uses signals, slots, or properties must have Q_OBJECT in its class body and inherit from QObject:

class Sensor : public QObject {
    Q_OBJECT          // <-- required for MOC processing

public:
    explicit Sensor(QObject *parent = nullptr);
};

Without Q_OBJECT, signals/slots and connect() won’t work. The Meta-Object Compiler (MOC) reads this macro and generates the required boilerplate.


Q_PROPERTY — Runtime Properties

Q_PROPERTY exposes class members to Qt’s meta-object system, enabling access from QML, stylesheets, and serialization:

class TemperatureSensor : public QObject {
    Q_OBJECT
    Q_PROPERTY(float temperature READ temperature NOTIFY temperatureChanged)
    Q_PROPERTY(QString unit READ unit WRITE setUnit NOTIFY unitChanged)
    Q_PROPERTY(bool connected READ isConnected CONSTANT)

public:
    explicit TemperatureSensor(QObject *parent = nullptr);

    float temperature() const { return m_temperature; }
    QString unit() const { return m_unit; }
    bool isConnected() const { return m_connected; }

public slots:
    void setUnit(const QString &unit) {
        if (m_unit != unit) {
            m_unit = unit;
            emit unitChanged(m_unit);
        }
    }

    void readSensor() {
        m_temperature = 36.5f;  // read from hardware
        emit temperatureChanged(m_temperature);
    }

signals:
    void temperatureChanged(float temp);
    void unitChanged(const QString &unit);

private:
    float m_temperature = 0.0f;
    QString m_unit = "°C";
    bool m_connected = true;
};

Accessing properties dynamically:

TemperatureSensor sensor;

// Dynamic property access via meta-object
QVariant temp = sensor.property("temperature");
qDebug() << "Temperature:" << temp.toFloat();

sensor.setProperty("unit", "°F");

Object Trees

QObject objects form trees. Use children() and findChild() to navigate:

QObject root;
root.setObjectName("root");

QObject *bus  = new QObject(&root);  bus->setObjectName("I2C_Bus");
QObject *dev1 = new QObject(bus);    dev1->setObjectName("Sensor_0x48");
QObject *dev2 = new QObject(bus);    dev2->setObjectName("Sensor_0x49");

// Find a child by name
QObject *found = root.findChild<QObject*>("Sensor_0x48");
if (found) qDebug() << "Found:" << found->objectName();

// Find all children of a type
QList<QObject*> all = root.findChildren<QObject*>();
qDebug() << "Total objects:" << all.size();

Runtime Type Information — qobject_cast

Use qobject_cast instead of dynamic_cast for QObject-derived types:

QObject *obj = createDevice();  // returns QObject*

// Safe downcast
if (auto *sensor = qobject_cast<TemperatureSensor*>(obj)) {
    qDebug() << "Temp:" << sensor->temperature();
} else {
    qDebug() << "Not a TemperatureSensor";
}

qobject_cast returns nullptr on failure (no exceptions) and works without RTTI enabled — important for embedded targets.


QObject — What NOT to Do

// BAD: QObject cannot be copied
QObject a;
QObject b = a;  // compile error — copy constructor is deleted

// BAD: stack-allocating a child then giving it a parent
{
    QObject child;
    QObject parent;
    child.setParent(&parent);
}  // double-delete! child destroyed by scope AND by parent

// CORRECT: heap-allocate children with parent pointer
QObject parent;
QObject *child = new QObject(&parent);  // parent deletes child

Summary

FeatureDescription
Q_OBJECTRequired for MOC, signals/slots, properties
Parent-childParent auto-deletes all children
Q_PROPERTYExpose members to meta-object system and QML
findChild()Navigate object trees at runtime
qobject_castSafe, no-exception downcast for QObjects
No copyingQObject is non-copyable by design

Advanced Topics

Dynamic Properties

QObject obj;
// Set arbitrary properties at runtime — no Q_PROPERTY needed
obj.setProperty("baudRate", 115200);
obj.setProperty("deviceName", "ttyUSB0");

QVariant rate = obj.property("baudRate");
qDebug() << rate.toInt();  // 115200

Object Ownership Patterns

// Pattern 1: Stack root, heap children (most common)
QWidget window;                              // stack
QLabel  *label = new QLabel("Hi", &window); // heap child

// Pattern 2: Shared ownership via QSharedPointer (no parent)
QSharedPointer<QObject> obj(new QObject);

// Pattern 3: Scoped (no parent, auto-delete)
QScopedPointer<QObject> scoped(new QObject);

Custom Meta-Object Info

class Protocol : public QObject {
    Q_OBJECT
    Q_CLASSINFO("version",  "1.2")
    Q_CLASSINFO("protocol", "UART/I2C")
public:
    explicit Protocol(QObject *parent = nullptr) : QObject(parent) {}
};

// Access at runtime
const QMetaObject *mo = Protocol::staticMetaObject();
for (int i = 0; i < mo->classInfoCount(); ++i) {
    qDebug() << mo->classInfo(i).name() << mo->classInfo(i).value();
}
// Output:
// version  1.2
// protocol UART/I2C

Invoking Methods Dynamically

class Actuator : public QObject {
    Q_OBJECT
public slots:
    void activate()  { qDebug() << "Activated!"; }
    int  getStatus() { return 42; }
};

Actuator act;
// Invoke at runtime — useful for plugins
QMetaObject::invokeMethod(&act, "activate");

int result = 0;
QMetaObject::invokeMethod(&act, "getStatus",
    Q_RETURN_ARG(int, result));
qDebug() << result;  // 42

Visual: Qt Object Tree

QWidget (root)
│
├── QVBoxLayout
│
├── QLabel ("Title")
│
├── QPushButton ("Connect")
│
└── QFrame (panel)
    ├── QLineEdit (input)
    └── QSpinBox  (value)

When QWidget is destroyed → entire tree is deleted automatically

Lab 1 — Sensor Registry with findChild

QObject registry;
registry.setObjectName("SensorRegistry");

// Create named sensors
auto *temp = new QObject(&registry); temp->setObjectName("temp_0x48");
auto *humi = new QObject(&registry); humi->setObjectName("humi_0x50");
auto *pres = new QObject(&registry); pres->setObjectName("pres_0x60");

// Look up by name
QObject *found = registry.findChild<QObject*>("humi_0x50");
if (found)
    qDebug() << "Found sensor:" << found->objectName();

// List all sensors
const auto all = registry.findChildren<QObject*>();
qDebug() << "Registered sensors:" << all.size();
for (auto *s : all)
    qDebug() << " -" << s->objectName();

Lab 2 — Dynamic Property Config

class DeviceConfig : public QObject {
    Q_OBJECT
public:
    void loadDefaults() {
        setProperty("baud",    9600);
        setProperty("parity",  "none");
        setProperty("databits", 8);
        setProperty("stopbits", 1);
    }

    void printAll() {
        const auto names = dynamicPropertyNames();
        for (const auto &n : names)
            qDebug() << n << "=" << property(n.constData()).toString();
    }
};

DeviceConfig cfg;
cfg.loadDefaults();
cfg.setProperty("baud", 115200);  // override one
cfg.printAll();
// baud = 115200
// parity = none
// databits = 8
// stopbits = 1

Interview Questions

Q1: Why is QObject non-copyable?

QObject has an object identity — it may have connections, a parent, children, and a thread affinity. Copying would create ambiguous ownership and duplicate connections. The copy constructor and assignment operator are deleted.

Q2: What does MOC do?

The Meta-Object Compiler (MOC) reads C++ header files and generates a moc_*.cpp file containing the implementation of signals, the metaObject() vtable entry, and qt_metacall dispatcher. It enables runtime introspection without RTTI.

Q3: What’s the difference between Q_PROPERTY with READ/WRITE vs MEMBER?

READ/WRITE requires getter/setter functions. MEMBER directly binds to a class member variable — Qt generates the getter/setter automatically. MEMBER is concise; READ/WRITE allows custom logic.

Q4: When should you use deleteLater() instead of delete?

When inside a slot or signal handler — calling delete this can crash if the event loop still holds a reference. deleteLater() schedules deletion after the current event loop iteration completes safely.

Q5: How does qobject_cast differ from dynamic_cast?

qobject_cast uses Qt’s meta-object system and works without RTTI (important for embedded). It returns nullptr on failure. dynamic_cast requires RTTI and throws exceptions on some compilers.


Application: Plugin-Style Device Registry

// idevice.h — interface
class IDevice : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString deviceId READ deviceId CONSTANT)
public:
    explicit IDevice(const QString &id, QObject *parent = nullptr)
        : QObject(parent), m_id(id) {}

    QString deviceId() const { return m_id; }
    virtual void initialize() = 0;
    virtual QString status() const = 0;

signals:
    void statusChanged(const QString &newStatus);

private:
    QString m_id;
};

// uart_device.h
class UartDevice : public IDevice {
    Q_OBJECT
public:
    UartDevice(QObject *p=nullptr) : IDevice("uart-0", p) {}
    void initialize() override { qDebug() << "UART init"; }
    QString status() const override { return "running"; }
};

// registry.h
class DeviceRegistry : public QObject {
    Q_OBJECT
public:
    void registerDevice(IDevice *dev) {
        dev->setParent(this);  // registry owns device
        m_devices[dev->deviceId()] = dev;
        QObject::connect(dev, &IDevice::statusChanged,
            [this, dev](const QString &s) {
                qDebug() << dev->deviceId() << "→" << s;
            });
    }

    IDevice* find(const QString &id) {
        return m_devices.value(id, nullptr);
    }

    void initAll() {
        for (auto *d : m_devices) d->initialize();
    }

private:
    QHash<QString, IDevice*> m_devices;
};

// main.cpp
DeviceRegistry registry;
registry.registerDevice(new UartDevice);

if (auto *dev = qobject_cast<UartDevice*>(registry.find("uart-0")))
    qDebug() << "Status:" << dev->status();

registry.initAll();

References

ResourceLink
Qt Docs: QObjecthttps://doc.qt.io/qt-6/qobject.html
Qt Docs: Object Treeshttps://doc.qt.io/qt-6/objecttrees.html
Qt Docs: Q_PROPERTYhttps://doc.qt.io/qt-6/properties.html
Qt Docs: Meta-Object Systemhttps://doc.qt.io/qt-6/metaobjects.html

Next tutorial → QML & Qt Quick