Qt Signals, Slots, and Meta-Object


  • Description: How Qt's signal/slot mechanism works — Q_OBJECT and moc, the modern functor connect syntax vs the legacy SIGNAL/SLOT macros, connection types (direct, queued, blocking-queued, auto), the Meta-Object System (qobject_cast, tr, Q_PROPERTY, Q_INVOKABLE)
  • My Notion Note ID: K2A-B3-4
  • Created: 2018-03-04
  • Updated: 2026-05-18
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Why Signals and Slots

  • Decouples the emitter of an event from the handler. The button doesn't know what happens when clicked; some other code says "when this button emits clicked, run my function".
  • Same idea as observer pattern / callbacks / function pointers — Qt's twist is that the wiring is type-safe (in modern syntax), thread-aware, and works across object lifetimes (auto-disconnects on destroy).
  • The cost: every emitter and slot owner must inherit from QObject (or a subclass), and the class needs Q_OBJECT so the build-time moc tool can generate the meta-object table.

2. Q_OBJECT and moc

class Counter : public QObject {
    Q_OBJECT                                 // required — even with no signals/slots, for tr() and qobject_cast
public:
    explicit Counter(QObject *parent = nullptr);
    int value() const { return value_; }

public slots:
    void increment();

signals:
    void valueChanged(int newValue);

private:
    int value_ = 0;
};
  • Q_OBJECT is a macro recognized by moc (Meta-Object Compiler). At build time, moc reads the header and emits a moc_counter.cpp containing:
    • The static meta-object table (signal/slot names, signatures, properties).
    • The signal implementations (you only declare them — moc writes the bodies that pack args and call QMetaObject::activate).
    • Dispatch glue for dynamic invocation, qobject_cast, Q_PROPERTY.
  • Forgetting Q_OBJECT is the #1 Qt build error: "undefined reference to vtable for MyClass" / "signal not declared" / "qobject_cast not finding the right type". Add Q_OBJECT, rerun cmake/qmake. With AUTOMOC ON (CMake) or HEADERS += myclass.h (qmake), no manual moc invocation is needed.
  • Out-of-source declarations: if you define a Q_OBJECT class in a .cpp (e.g., a small helper), end the file with #include "myfile.moc" so moc runs and links.

3. Declaring Signals and Slots

  • Signals are declared in a signals: access section (no body — moc writes them).
  • Slots are declared in public slots: / protected slots: / private slots: — same as ordinary member functions, but visible to the meta-object system. Since Qt 5 you can also connect to any function (member or free or lambda) — slots: is only needed for QML access, dynamic invocation, or the legacy macro syntax.
class MyForm : public QWidget {
    Q_OBJECT
public slots:
    void onSubmit();
    void setName(const QString &name);

signals:
    void submitted(const QString &name);     // no body — moc writes one
};

void MyForm::onSubmit() {
    emit submitted(name_);                   // "emit" is sugar; really just a normal call
}
  • emit is a no-op macro — emit signal(x); is identical to signal(x);. It exists to make intent obvious to readers; some teams omit it, but the convention is to keep it.

4. The Functor connect Syntax (Qt 5+)

  • Type-safe, compile-time-checked, supports lambdas and member-function pointers. Use this for all new code.
// Member-function slot
connect(submitBtn, &QPushButton::clicked,
        this,      &MyForm::onSubmit);

// Lambda — convenient, but watch lifetime
connect(submitBtn, &QPushButton::clicked,
        this, [this] { onSubmit(); });

// Free function
connect(submitBtn, &QPushButton::clicked,
        &logSubmission);

// Signal-to-signal forwarding
connect(submitBtn, &QPushButton::clicked,
        this,      &MyForm::submitted);    // wait — needs a slot or another signal to emit
  • Compile-time check: signal and slot signatures must be compatible (slot may have fewer arguments). If they don't match, you get a real error message, not the silent runtime warning the legacy syntax gives you.
  • Lambdas: always pass a context object (the 3rd arg) so Qt can auto-disconnect when that object dies. connect(btn, &QPushButton::clicked, [this]{ ... }) without context is a use-after-free waiting to happen.
  • Overload disambiguation — when a signal is overloaded (e.g., QComboBox::currentIndexChanged(int) and (QString)), use qOverload:
connect(combo, qOverload<int>(&QComboBox::currentIndexChanged),
        this,  &MyForm::onPick);

5. Legacy String-Based SIGNAL/SLOT

  • The Qt 4 way; still works, no longer recommended.
connect(submitBtn, SIGNAL(clicked()),
        this,     SLOT(onSubmit()));
  • Checked at runtime only. A typo in the signal name fails silently with a qWarning("QObject::connect: No such signal ...") that's easy to miss in log noise.
  • Doesn't work with lambdas or non-slot functions.
  • One advantage: works with QML interop and dynamic signal lookup (rare). Keep it in mind when reading old code; don't write new code with it.

6. Connection Types

  • Qt's signal/slot wiring is thread-aware. The 5th connect argument picks how the slot is invoked:
Type Meaning
Qt::AutoConnection (default) Direct if emitter and receiver live in the same thread; Queued otherwise.
Qt::DirectConnection Slot runs immediately in the emitter's thread, before emit returns. Like a normal function call.
Qt::QueuedConnection Slot is posted as an event to the receiver's thread; runs when that thread's event loop dispatches it. Args are copied; types must be registered (qRegisterMetaType) for custom types.
Qt::BlockingQueuedConnection Like queued, but the emitter blocks until the slot finishes. Receiver must be in a different thread (else self-deadlock). Useful for cross-thread "return a value" patterns.
Qt::UniqueConnection OR-able flag — refuses to add a duplicate connection.
connect(worker, &Worker::progress,
        this,   &MainWindow::onProgress,
        Qt::QueuedConnection);

connect(button, &QPushButton::clicked,
        this,   &MyForm::onClick,
        Qt::UniqueConnection);
  • For most desktop apps, defaults are right — AutoConnection does what you want.
  • Cross-thread custom types: qRegisterMetaType<MyStruct>("MyStruct"); once at startup, otherwise QueuedConnection warns "Cannot queue arguments of type 'MyStruct'" and drops the call.

7. Disconnecting

  • The functor connect returns a QMetaObject::Connection you can pass to disconnect:
QMetaObject::Connection c = connect(button, &QPushButton::clicked, this, &Form::onClick);
disconnect(c);
  • Most of the time you don't need to disconnect explicitly: when either the sender or receiver (or the context object for a lambda) is destroyed, Qt cleans up.
  • disconnect(button, nullptr, this, nullptr) — removes every connection between button and this. Wider hammer; use sparingly.

8. Auto-Connection by Name (on_<obj>_<signal>)

  • Pattern Qt Designer's generated code relies on: a slot named on_<objectName>_<signalName> is connected automatically when you call QMetaObject::connectSlotsByName(this) in setupUi.
// objectName: "openButton"; signal: clicked()
void MainWindow::on_openButton_clicked() { ... }
  • Designer-generated ui_*.h calls connectSlotsByName for you. If you write a class by hand and forget the call, the auto-connections silently never run.
  • It's brittle — a typo in the slot name leaves the slot dangling with no error. The modern recommendation is to write explicit connect(...) calls instead.

9. The Meta-Object System

  • Q_OBJECT does more than signals/slots. It gives you a runtime type system on top of plain C++.

9.1 qobject_cast

QWidget *w = ...;
if (auto *btn = qobject_cast<QPushButton *>(w)) {
    btn->setEnabled(false);
}
  • Like dynamic_cast, but uses Qt's meta-object table rather than RTTI. Slightly faster than dynamic_cast on most platforms; more importantly, works across DLL boundaries with disabled RTTI (e.g., Visual Studio + Qt plugins).
  • Returns nullptr if the cast fails. Only works on classes with Q_OBJECT.

9.2 tr and Internationalization

auto *label = new QLabel(tr("Welcome"));
  • tr(s) is a static method generated by Q_OBJECT. At runtime it looks up s in the loaded QTranslator for the current locale and returns the translation, or s itself if no translation exists.
  • lupdate (Qt's translation source tool) walks .cpp/.ui files, extracts every tr(...) call into an XML .ts file. Translators edit .ts, lrelease compiles to .qm, your binary loads .qm via QTranslator.
  • For classes that aren't QObject (e.g., a struct of strings), add Q_DECLARE_TR_FUNCTIONS(MyClass) in the class body to get a static tr.

9.3 Q_PROPERTY

  • Declares a property that the meta-object system can read/write by name. Powers QML bindings, animation, style sheets, and QObject::setProperty.
class Player : public QObject {
    Q_OBJECT
    Q_PROPERTY(int    volume   READ volume   WRITE setVolume NOTIFY volumeChanged)
    Q_PROPERTY(QString title   READ title    CONSTANT)
public:
    int volume() const { return volume_; }
    void setVolume(int v) {
        if (v == volume_) return;
        volume_ = v;
        emit volumeChanged(v);
    }
    QString title() const { return title_; }
signals:
    void volumeChanged(int);
};

Player p;
p.setProperty("volume", 75);                  // by-name; same as p.setVolume(75)
QVariant v = p.property("volume");            // returns 75
  • NOTIFY is the signal that fires on changes — required for QML bindings and QPropertyAnimation.
  • CONSTANT declares an immutable property — no setter or notify needed; the value never changes after construction.

9.4 Q_INVOKABLE and Dynamic Invocation

  • Marks a regular method (not a slot) as callable via QMetaObject::invokeMethod. Used by QML and scripting engines.
class Calculator : public QObject {
    Q_OBJECT
public:
    Q_INVOKABLE int add(int a, int b) const { return a + b; }
};

Calculator c;
int result;
QMetaObject::invokeMethod(&c, "add",
                          Q_RETURN_ARG(int, result),
                          Q_ARG(int, 2), Q_ARG(int, 3));   // result == 5
  • invokeMethod also accepts an invocation type (Qt::QueuedConnection etc.), so it's the idiomatic way to dispatch a one-shot call to another thread's event loop without writing a slot.

10. Custom Signals with Arguments

class FileLoader : public QObject {
    Q_OBJECT
public:
    void load(const QString &path);

signals:
    void progress(qint64 bytesRead, qint64 bytesTotal);
    void finished(const QByteArray &data);
    void failed(const QString &error);
};

void FileLoader::load(const QString &path) {
    // ... read file, emit progress periodically ...
    emit progress(bytesRead, bytesTotal);
    // ...
    emit finished(allBytes);
}
  • A signal can have any arguments your build allows — including templates and custom types. For cross-thread (queued) connections, register custom types once: qRegisterMetaType<MyStruct>("MyStruct") at app startup.
  • Pass complex objects by const &. They're copied across thread boundaries automatically (QueuedConnection always copies); within a thread, the compiler will elide the copy.

11. References