Signals and Slots
How Nt objects notify each other without knowing about each other.
Objects often need to react to each other: a label shows a new value, a timer starts work, a network reply hands over its data. In Nt, as in Qt, the object where something happens emits a signal, and every function connected to that signal, a slot, is called. The sender does not know who is listening, so classes stay independent and can be reused and tested alone.
This page builds up the pattern step by step with the signals example in examples/signals, which checks each step when it runs. Build it on its own against an installed Nt:
cmake -S examples/signals -B build-signals
cmake --build build-signals
build-signals/signals
Declaring Signals and Slots
A class with signals derives from QObject, starts with Q_OBJECT, and lists its signals in a signals: section. Signals are only declared; ntmoc writes their bodies. Slots are ordinary member functions:
// A class with signals and slots derives from QObject and starts with Q_OBJECT. ntmoc reads the
// header and writes the code behind the signals; nt_enable_automoc() in CMake runs it.
class Thermostat : public QObject {
Q_OBJECT
public:
explicit Thermostat(QObject *parent = nullptr) : QObject(parent) {}
double temperature() const { return m_temperature; }
public slots:
// A slot is an ordinary member function. Putting it under "slots" also lets it be connected
// by name with SLOT() and called with QMetaObject::invokeMethod().
void setTemperature(double celsius) {
if (celsius == m_temperature)
return; // emit only on a real change, so connected objects cannot loop forever
m_temperature = celsius;
emit temperatureChanged(celsius);
if (celsius > 30.0)
emit overheated();
}
signals:
// Signals are declared, never defined: ntmoc writes their bodies.
void temperatureChanged(double celsius);
void overheated();
private:
double m_temperature = 20.0;
};
emit is an empty word that marks the call as a signal for the reader. Emit a signal only when something really changed, as setTemperature() does: when two objects are connected to each other, this is what stops them from updating each other forever.
The receiving class is a QObject too, so that Nt can remove its connections when it is destroyed:
class Display : public QObject {
Q_OBJECT
public:
QString text;
public slots:
void show(double celsius) { text = QString::number(celsius) + " C"; }
};
Building with ntmoc
ntmoc must process every header that declares a Q_OBJECT class. With CMake, list the header among the sources of the target and call nt_enable_automoc(), as for CMAKE_AUTOMOC with Qt:
find_package(Nt6 6 REQUIRED)
add_executable(signals main.cpp thermostat.h)
target_link_libraries(signals PRIVATE Nt6::Core)
nt_enable_automoc(signals)
If the linker reports an undefined reference to a signal, or to staticMetaObject, the class was not processed: check that the header is listed and that it has Q_OBJECT.
Connecting a Signal to a Slot
QObject::connect() takes the sender, a pointer to the signal, the receiver and a pointer to the slot. The compiler checks that the slot can take the signal's arguments:
Thermostat thermostat;
Display display;
// Sender, &Class::signal, receiver, &Class::slot.
QObject::connect(&thermostat, &Thermostat::temperatureChanged, &display, &Display::show);
thermostat.setTemperature(21.5);
// display.text is now "21.5 C": the slot ran inside setTemperature().
By default the slot runs at once, inside the emit, before setTemperature() returns. A signal can be connected to many slots, which run in the order they were connected, and a slot can be connected to many signals.
Lambdas and Other Functions
Any callable can be a slot: a lambda, a free function or a member of a class that is not a QObject. Pass a context object before the lambda: the connection lives only as long as the context, and with threads, the lambda runs in the context's thread:
int alarms = 0;
// A lambda needs no receiver. Give it a context object anyway (here &display) when it uses
// something that can be destroyed: the connection is removed together with the context.
QObject::connect(&thermostat, &Thermostat::overheated, &display, [&alarms] { ++alarms; });
thermostat.setTemperature(35.0);
Warning: A lambda that captures this, or a reference to a local variable, must not outlive what it captures. Connecting it with this, or the object it uses, as the context object removes the connection in time.
Arguments
A slot may take fewer arguments than the signal; the trailing ones are dropped. The types must match or convert implicitly:
// A slot may ignore the trailing arguments of the signal.
int changes = 0;
QObject::connect(&thermostat, &Thermostat::temperatureChanged, [&changes] { ++changes; });
When a signal is overloaded, name the overload with qOverload, for example qOverload<int>(&QSpinBox::valueChanged).
Lifetime and Disconnecting
When the sender or the receiver is destroyed, its connections are removed, so a slot is never called on a deleted object:
{
auto window = std::make_unique<Display>();
QObject::connect(&thermostat, &Thermostat::temperatureChanged, window.get(), &Display::show);
} // the Display is destroyed here, and so is its connection
thermostat.setTemperature(19.0); // safe: nothing calls into the destroyed object
To stop listening earlier, keep the QMetaObject::Connection that connect() returns, or disconnect by sender, signal and receiver:
const QMetaObject::Connection logging =
QObject::connect(&thermostat, &Thermostat::temperatureChanged, [](double c) { std::printf("%.1f\n", c); });
QObject::disconnect(logging); // remove just this connection
// ...or every connection from a signal to one receiver:
QObject::disconnect(&thermostat, &Thermostat::temperatureChanged, &display, nullptr);
Inside a slot, sender() returns the object that emitted the signal. Do not delete the sender from its own signal; call deleteLater() on it instead, which deletes it once control returns to the event loop.
Connection Flags
Two flags change how often a slot runs. Combine them with a connection type with | and a cast to Qt::ConnectionType:
// Called once, then disconnected automatically.
int firstReading = 0;
QObject::connect(
&thermostat, &Thermostat::temperatureChanged, &display, [&firstReading] { ++firstReading; },
Qt::SingleShotConnection);
// Not connected twice, however often this line runs.
QObject::connect(&thermostat, &Thermostat::temperatureChanged, &display, &Display::show, Qt::UniqueConnection);
QObject::connect(&thermostat, &Thermostat::temperatureChanged, &display, &Display::show, Qt::UniqueConnection);
Qt::UniqueConnection recognizes member function slots and slots named with SLOT(); every lambda is a new slot.
Queued Connections and the Event Loop
A queued connection copies the arguments and calls the slot later, from the event loop of the receiver's thread. Connections between threads are queued automatically. Nothing is delivered until the event loop runs, which QCoreApplication::exec() does:
// A queued connection runs the slot later, from the event loop, as connections to objects in
// other threads do. Nothing happens until the loop runs.
double queued = 0;
QObject::connect(
&thermostat, &Thermostat::temperatureChanged, &display, [&queued](double c) { queued = c; },
Qt::QueuedConnection);
thermostat.setTemperature(22.0);
// queued is still 0 here.
QTimer::singleShot(0, &app, &QCoreApplication::quit);
app.exec(); // the slot runs now: queued becomes 22
The arguments of a queued signal must be copyable types that the meta-type system knows: built-in types, Nt's value classes, and your own types registered with Q_DECLARE_METATYPE().
Common Mistakes
Nothing happens. The connection failed or was never made. connect() returns a QMetaObject::Connection that converts to
falsewhen it failed, and string-based connections print a warning.A queued slot, a timer or a reply never fires. No event loop is running. Call QCoreApplication::exec(), or use a QEventLoop while waiting.
A crash in a lambda. It used an object that was destroyed. Pass that object as the context of the connection.
A slot is called twice. The same connection was made twice, for example each time a dialog opens. Connect once, disconnect first, or use
Qt::UniqueConnection.Endless updates. Two objects set each other's values. Emit only when the value changes.
A signal emitted in a constructor is lost. Nobody could connect to the object yet.
Delivery
Connection type | Behaviour |
|---|---|
| The slot runs immediately, inside the emit. |
| The arguments are copied and the slot runs from the event loop. |
| Like |
| The default. Direct if the receiver lives in the emitting thread, queued otherwise. |
Threads
Every object lives in a thread: the one that created it, or the one it was moved to with QObject::moveToThread(). That thread's event loop runs the object's timers and delivers its posted events and queued signals. Qt::AutoConnection compares the receiver's thread with the emitting thread each time the signal is emitted, so a signal emitted in a worker thread reaches a slot of an object in the main thread through the main thread's event loop:
auto *worker = new Worker;
worker->moveToThread(&thread);
QObject::connect(&thread, &QThread::finished, worker, &QObject::deleteLater);
QObject::connect(this, &Controller::start, worker, &Worker::run); // queued
QObject::connect(worker, &Worker::result, this, &Controller::show); // queued
thread.start();
Connecting, disconnecting and emitting are safe from any thread. An object must be deleted from its own thread, or while its thread is not running events for it; use deleteLater() to delete an object in another thread.
String-based Connections
The SIGNAL() and SLOT() macros name a signal or slot by its signature, which lets you connect to members that you only know at run time. The class must have gone through ntmoc, which records its signals and slots:
QObject::connect(&counter, SIGNAL(valueChanged(int)), &display, SLOT(show(int)));
A connection that names a member that does not exist, or one whose arguments do not match, prints a warning and is not made. Signals that a class declares with a body of its own, and classes that ntmoc has not processed, can only be connected with the pointer-to-member syntax.