- 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
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);
};
void MyForm::onSubmit() {
emit submitted(name_);
}
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.
connect(submitBtn, &QPushButton::clicked,
this, &MyForm::onSubmit);
connect(submitBtn, &QPushButton::clicked,
this, [this] { onSubmit(); });
connect(submitBtn, &QPushButton::clicked,
&logSubmission);
connect(submitBtn, &QPushButton::clicked,
this, &MyForm::submitted);
- 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.
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.
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);
QVariant v = p.property("volume");
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));
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) {
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