Signals & Slots

Qt's event-driven communication mechanism — connecting objects, lambda slots, and thread-safe signal emission.

6 min read
46255 chars

What Are Signals & Slots?

Signals and slots are Qt’s mechanism for loose-coupled communication between objects. A signal is emitted when something happens; a slot is a function that responds to that signal. No direct dependency between sender and receiver.

[Sender Object]  --signal-->  [Receiver Object]
     button          clicked        handler()

Declaring Signals & Slots

A class must inherit from QObject and include the Q_OBJECT macro:

// counter.h
#include <QObject>

class Counter : public QObject {
    Q_OBJECT

public:
    explicit Counter(QObject *parent = nullptr);
    int value() const { return m_value; }

public slots:
    void setValue(int value);
    void increment();

signals:
    void valueChanged(int newValue);

private:
    int m_value = 0;
};
// counter.cpp
#include "counter.h"

Counter::Counter(QObject *parent) : QObject(parent) {}

void Counter::setValue(int value) {
    if (value != m_value) {
        m_value = value;
        emit valueChanged(m_value);  // emit the signal
    }
}

void Counter::increment() {
    setValue(m_value + 1);
}

Connecting Signals to Slots

Counter a, b;

// Connect a's signal to b's slot
QObject::connect(&a, &Counter::valueChanged,
                 &b, &Counter::setValue);

a.setValue(10);
// b.value() is now 10

Lambda Slot

Counter counter;

QObject::connect(&counter, &Counter::valueChanged,
                 [](int value) {
                     qDebug() << "Value changed to:" << value;
                 });

counter.setValue(42);
// Output: Value changed to: 42

Old String-Based Syntax (avoid in new code)

// Legacy — no compile-time type checking
connect(&a, SIGNAL(valueChanged(int)),
        &b, SLOT(setValue(int)));

Built-in Widget Signals

Qt widgets come with many built-in signals:

#include <QApplication>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget window;
    QVBoxLayout *layout = new QVBoxLayout(&window);

    QPushButton *btn = new QPushButton("Click Me");
    QLabel *label = new QLabel("Waiting...");

    layout->addWidget(btn);
    layout->addWidget(label);

    int clickCount = 0;
    QObject::connect(btn, &QPushButton::clicked, [&]() {
        clickCount++;
        label->setText(QString("Clicked %1 times").arg(clickCount));
    });

    window.show();
    return app.exec();
}

Signal-to-Signal Connections

Signals can be chained — one signal triggers another:

QPushButton *btn = new QPushButton("Quit");
QObject::connect(btn, &QPushButton::clicked,
                 &app, &QApplication::quit);

Thread-Safe Signal Emission

When sender and receiver are in different threads, Qt uses queued connections automatically:

// Explicit queued connection (cross-thread)
QObject::connect(&worker, &Worker::resultReady,
                 &ui,     &UI::displayResult,
                 Qt::QueuedConnection);

// Auto-detect (default): uses queued if different threads
QObject::connect(&worker, &Worker::resultReady,
                 &ui,     &UI::displayResult,
                 Qt::AutoConnection);

Connection types:

TypeBehavior
Qt::AutoConnectionDirect if same thread, queued if different
Qt::DirectConnectionSlot called immediately in sender’s thread
Qt::QueuedConnectionSlot called in receiver’s thread via event loop
Qt::BlockingQueuedConnectionQueued + blocks sender until slot returns

Disconnecting

// Disconnect a specific connection
QMetaObject::Connection conn = QObject::connect(...);
QObject::disconnect(conn);

// Disconnect all signals from an object
QObject::disconnect(&sender, nullptr, nullptr, nullptr);

// Disconnect a specific signal from a receiver
QObject::disconnect(&sender, &Sender::mySignal,
                    &receiver, &Receiver::mySlot);

Summary

ConceptKey Point
signals:Declare with no implementation — Qt generates them
slots:Regular member functions callable via connect
emitKeyword to fire a signal
Q_OBJECTRequired macro for MOC processing
Lambda slotsClean, concise — capture with [&] carefully
Thread safetyUse Qt::QueuedConnection across threads

Advanced Topics

1. Custom Signal with Multiple Parameters

class GPSSensor : public QObject {
    Q_OBJECT
signals:
    void locationUpdated(double lat, double lon, float accuracy);
};

// Connect
QObject::connect(&gps, &GPSSensor::locationUpdated,
    [](double lat, double lon, float acc) {
        qDebug() << "GPS:" << lat << lon << "±" << acc << "m";
    });

2. Overloaded Signals — Use QOverload

class Display : public QObject {
    Q_OBJECT
signals:
    void updated(int value);
    void updated(const QString &text);
};

// Must resolve overload explicitly
QObject::connect(&display,
    QOverload<int>::of(&Display::updated),
    [](int v){ qDebug() << "Int:" << v; });

3. Blocking Connection (Cross-Thread Sync)

// Blocks caller thread until slot in receiver thread completes
QObject::connect(&worker, &Worker::dataReady,
                 &db,     &Database::store,
                 Qt::BlockingQueuedConnection);

4. Connection Guard (RAII disconnect)

QMetaObject::Connection conn =
    QObject::connect(&btn, &QPushButton::clicked, handler);

// Later: disconnect cleanly
QObject::disconnect(conn);

5. Signal Spy (for testing)

#include <QSignalSpy>

QSignalSpy spy(&counter, &Counter::valueChanged);
counter.setValue(5);

QCOMPARE(spy.count(), 1);
QCOMPARE(spy.at(0).at(0).toInt(), 5);

Visual: Connection Types

┌─────────────────────────────────────────────────────────┐
│                  Qt Connection Types                     │
├─────────────────┬───────────────────────────────────────┤
│  AutoConnection │ Same thread → Direct                  │
│                 │ Diff thread → Queued                   │
├─────────────────┼───────────────────────────────────────┤
│  Direct         │ Slot runs inline in sender's thread   │
├─────────────────┼───────────────────────────────────────┤
│  Queued         │ Event posted → slot in receiver thread│
├─────────────────┼───────────────────────────────────────┤
│ BlockingQueued  │ Queued + sender thread blocks          │
├─────────────────┼───────────────────────────────────────┤
│  UniqueConn     │ Prevents duplicate connections        │
└─────────────────┴───────────────────────────────────────┘

Lab 1 — Temperature Monitor

Goal: Connect a sensor that emits temperature readings to a display widget.

// sensor.h
class TemperatureSensor : public QObject {
    Q_OBJECT
public:
    void simulate() {
        for (int i = 0; i < 5; ++i) {
            float temp = 20.0f + (rand() % 100) / 10.0f;
            emit temperatureRead(temp);
            QThread::msleep(500);
        }
    }
signals:
    void temperatureRead(float celsius);
};

// display.h
class TempDisplay : public QObject {
    Q_OBJECT
public slots:
    void showTemperature(float c) {
        qDebug() << QString("Temp: %1 °C").arg(c, 0, 'f', 1);
    }
};

// main.cpp
TemperatureSensor sensor;
TempDisplay       display;

QObject::connect(&sensor, &TemperatureSensor::temperatureRead,
                 &display, &TempDisplay::showTemperature);

sensor.simulate();

Expected Output:

Temp: 23.4 °C
Temp: 27.1 °C
Temp: 21.8 °C
...

Lab 2 — Event Bus Pattern

Goal: Implement a central event bus where multiple receivers subscribe to named events.

// eventbus.h
class EventBus : public QObject {
    Q_OBJECT
public:
    static EventBus& instance() {
        static EventBus bus;
        return bus;
    }
signals:
    void deviceConnected(const QString &deviceId);
    void alarmTriggered(int level, const QString &msg);
};

// usage
QObject::connect(&EventBus::instance(), &EventBus::alarmTriggered,
    [](int level, const QString &msg) {
        qDebug() << "[ALARM L" << level << "]" << msg;
    });

emit EventBus::instance().alarmTriggered(2, "High temperature!");

Interview Questions

Q1: What is the difference between Qt::DirectConnection and Qt::QueuedConnection?

Direct: slot executes immediately in sender’s thread. Queued: an event is posted to the receiver’s event queue; slot executes in the receiver’s thread. Use Queued for cross-thread safety.

Q2: Why can’t you use old SIGNAL/SLOT macros in new code?

They use string matching at runtime, offer no compile-time type checking, and are slower than function pointer syntax. A typo silently fails at runtime.

Q3: What happens if you connect() the same signal/slot pair twice?

The slot is called twice per emission. Use Qt::UniqueConnection flag to prevent duplicates.

Q4: How do you disconnect all connections from an object when it’s destroyed?

Qt does it automatically — when a QObject is destroyed, all connections involving it (as sender or receiver) are automatically cleaned up.

Q5: Can signals have return values?

No. Signals always return void. For return data, use another signal from the receiver back to the sender, or a shared data structure.


Application: IoT Device Manager

// device_manager.h
class DeviceManager : public QObject {
    Q_OBJECT
public:
    void addDevice(const QString &id) {
        m_devices.append(id);
        emit deviceAdded(id);
    }

    void removeDevice(const QString &id) {
        m_devices.removeAll(id);
        emit deviceRemoved(id);
    }

    int count() const { return m_devices.size(); }

signals:
    void deviceAdded(const QString &id);
    void deviceRemoved(const QString &id);
    void countChanged(int newCount);

private:
    QStringList m_devices;
};

// logger.h
class Logger : public QObject {
    Q_OBJECT
public slots:
    void onDeviceAdded(const QString &id) {
        qDebug() << "[LOG] Device connected:" << id;
    }
    void onDeviceRemoved(const QString &id) {
        qDebug() << "[LOG] Device disconnected:" << id;
    }
};

// main.cpp
DeviceManager mgr;
Logger        logger;
QLabel       *statusLabel = new QLabel;

QObject::connect(&mgr, &DeviceManager::deviceAdded,
                 &logger, &Logger::onDeviceAdded);
QObject::connect(&mgr, &DeviceManager::deviceRemoved,
                 &logger, &Logger::onDeviceRemoved);
QObject::connect(&mgr, &DeviceManager::deviceAdded,
    [&](const QString &id) {
        statusLabel->setText(QString("%1 device(s) online").arg(mgr.count()));
    });

mgr.addDevice("sensor-001");
mgr.addDevice("actuator-002");
mgr.removeDevice("sensor-001");

Output:

[LOG] Device connected: sensor-001
[LOG] Device connected: actuator-002
[LOG] Device disconnected: sensor-001

References

ResourceLink
Qt Docs: Signals & Slotshttps://doc.qt.io/qt-6/signalsandslots.html
Qt Docs: QObject::connecthttps://doc.qt.io/qt-6/qobject.html#connect
Qt Docs: Thread Supporthttps://doc.qt.io/qt-6/threads-qobject.html
Qt Docs: QSignalSpyhttps://doc.qt.io/qt-6/qsignalspy.html

Next tutorial → Qt Widgets — Building GUIs