K2A-B3-4
Qt Signals, Slots, and Meta-Object
Created 2018-03-04Updated 2026-05-18programming-language-stack / c_or_cpp / qt
- Description: How Qt's signal/slot mechanism works —
Q_OBJECTandmoc, the modern functorconnectsyntax vs the legacySIGNAL/SLOTmacros, 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
- 2.
Q_OBJECTandmoc - 3. Declaring Signals and Slots
- 4. The Functor
connectSyntax (Qt 5+) - 5. Legacy String-Based
SIGNAL/SLOT - 6. Connection Types
- 7. Disconnecting
- 8. Auto-Connection by Name (
on_<obj>_<signal>) - 9. The Meta-Object System
- 10. Custom Signals with Arguments
- 11. References
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 needsQ_OBJECTso the build-timemoctool 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_OBJECTis a macro recognized bymoc(Meta-Object Compiler). At build time,mocreads the header and emits amoc_counter.cppcontaining:- The static meta-object table (signal/slot names, signatures, properties).
- The signal implementations (you only declare them —
mocwrites the bodies that pack args and callQMetaObject::activate). - Dispatch glue for dynamic invocation,
qobject_cast,Q_PROPERTY.
- Forgetting
Q_OBJECTis the #1 Qt build error: "undefined reference tovtable for MyClass" / "signal not declared" / "qobject_cast not finding the right type". AddQ_OBJECT, reruncmake/qmake. WithAUTOMOC ON(CMake) orHEADERS += myclass.h(qmake), no manualmocinvocation is needed. - Out-of-source declarations: if you define a
Q_OBJECTclass in a.cpp(e.g., a small helper), end the file with#include "myfile.moc"somocruns and links.
3. Declaring Signals and Slots
- Signals are declared in a
signals:access section (no body —mocwrites 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
}
emitis a no-op macro —emit signal(x);is identical tosignal(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)), useqOverload:
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
connectargument 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 —
AutoConnectiondoes what you want. - Cross-thread custom types:
qRegisterMetaType<MyStruct>("MyStruct");once at startup, otherwiseQueuedConnectionwarns "Cannot queue arguments of type 'MyStruct'" and drops the call.
7. Disconnecting
- The functor
connectreturns aQMetaObject::Connectionyou can pass todisconnect:
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 betweenbuttonandthis. 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 callQMetaObject::connectSlotsByName(this)insetupUi.
// objectName: "openButton"; signal: clicked()
void MainWindow::on_openButton_clicked() { ... }
- Designer-generated
ui_*.hcallsconnectSlotsByNamefor 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_OBJECTdoes 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 thandynamic_caston most platforms; more importantly, works across DLL boundaries with disabled RTTI (e.g., Visual Studio + Qt plugins). - Returns
nullptrif the cast fails. Only works on classes withQ_OBJECT.
9.2 tr and Internationalization
auto *label = new QLabel(tr("Welcome"));
tr(s)is a static method generated byQ_OBJECT. At runtime it looks upsin the loadedQTranslatorfor the current locale and returns the translation, orsitself if no translation exists.lupdate(Qt's translation source tool) walks.cpp/.uifiles, extracts everytr(...)call into an XML.tsfile. Translators edit.ts,lreleasecompiles to.qm, your binary loads.qmviaQTranslator.- For classes that aren't
QObject(e.g., a struct of strings), addQ_DECLARE_TR_FUNCTIONS(MyClass)in the class body to get a statictr.
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
NOTIFYis the signal that fires on changes — required for QML bindings andQPropertyAnimation.CONSTANTdeclares 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
invokeMethodalso accepts an invocation type (Qt::QueuedConnectionetc.), 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 (QueuedConnectionalways copies); within a thread, the compiler will elide the copy.
11. References
- Qt 6 — Signals & Slots — canonical introduction with both syntaxes.
- Qt 6 — Differences between String-Based and Functor-Based Connections — side-by-side comparison.
- Qt 6 — The Meta-Object System —
Q_OBJECT,moc,Q_PROPERTY, dynamic invocation. - Qt 6 — Thread Support in Qt — how connection types interact with threads.
- Qt 6 —
qobject_cast— Qt's cross-DLL-safedynamic_castreplacement.