> ## Content Index
> Fetch the complete content index at: https://www.smallstepsystems.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Implementing Qt Signals and Slots in Pure C++
- URL: https://www.smallstepsystems.com/implementing-qt-signals-and-slots-in-pure-c-69076183a6eccd001b7aa59a/
- Published: 2022-08-28T18:31:23.000Z
- Updated: 2025-12-30T21:29:44.000Z
- Author: Burkhard Stubert

Signals and slots are my favourite Qt feature, as they make loose coupling between components or between layers super easy. I miss them most when I must write pure C++ code without the Qt goodies. I invite you to follow along while I translate signals and slots into function-object member variables and lambda functions, respectively.

## Motivation

Helen drives home in her car after a summer day in the mountains. She decreases the target temperature of the AC to 16 °C through the HMI of her car's infotainment system. The HMI application performs the following steps.

- When Helen presses the minus button to decrease the temperature, the QML view passes the temperature to its model by calling `ClimateModel::setTemperature` with decreasing values.
- The model stores the target temperature and calls the function `ClimateEcuTwin::setTemperature`.
- `ClimateEcuTwin` sends the target temperature over CAN bus to the physical climate ECU (Electronic Control Unit). The ECU produces enough cold air until the target temperature is reached in the car.

I start with the all to [normal C++ implementation](#tight-coupling-through-function-calls). `ClimateModel` calls `setTemperature` on a `ClimateEcuTwin` object directly. This couples `ClimateModel` and `ClimateEcuTwin` tightly. Qt developers know that they can break the tight coupling [with signals and slots](#loose-coupling-through-qt-signals-and-slots). Although I don't understand why, many projects forbid the use of Qt in the non-GUI parts. Fortunately, signals and slots can be implemented [in pure C++11](#loose-coupling-through-pure-c-signals-and-slots) with `std::function` and lambda functions.

You find the example code in the subdirectory *BlogPosts/SignalsSlotsInCpp* of my [GitHub repository embeddeduse](https://github.com/bstubert/embeddeduse/?ref=smallstepsystems.com). Just load the CMakeLists.txt file in the subdirectory into your IDE, build and run the command-line application. The first code snippet in each section gives you the commit sha of the example code.

## Tight Coupling through Function Calls

```
// main.cpp (Commit 5da4884)
ClimateEcuTwin twin;
ClimateModel model{&twin};
```

We create the object `twin` and pass it to the constructor of the `model`. The `ClimateModel` constructor stores `&twin` in the member variable `m_twin`.

```
// ClimateModel.cpp
ClimateModel::ClimateModel(ClimateEcuTwin *twin)
    : m_twin{twin}
{
}
```

When Helen decreases the target temperature, the QML view sets the temperature by calling, say, `model.setTemperature(16)` (in *main.cpp* for simplicity).

```
// ClimateModel.cpp
#include "ClimateEcuTwin.h"
...
void ClimateModel::setTemperature(int temperature)
{
    m_temperature = temperature;
    m_twin->setTemperature(m_temperature);
}
```

This function only compiles, if *ClimateModel.cpp* includes *ClimateEcuTwin.h*. The class `ClimateModel` depends on the class `ClimateEcuTwin`. If the climate ECU sends back the current interior temperature as a progress indicator, we introduce the reverse dependency and a dependency cycle. `ClimateEcuTwin` depends on `ClimateEcuTwin`. The two classes are tightly coupled.

The cyclic dependency forces us to deploy the two classes together, say, in a shared library. This would be sort of OK, if the two classes were in the same component, that is, in the same deployable unit. However, this is not the case for the [ports-and-adapters architecture](https://www.smallstepsystems.com/my-talk-a-successful-architecture-for-qt-embedded-systems-at-qt-day-italy-2021/) (see slides 7 and 8) and hence not for the three-layer architecture.

The class `ClimateModel` is part of the business logic. The class `ClimateEcuTwin` is part of the machine adapter. The business logic and the machine adapter are two different components. The business logic changes a lot more often than the machine adapter. Hence, both components are deployed at different times. Separate deployment is impossible with cyclic dependencies. Signals and slots come to the rescue.

## Loose Coupling through Qt Signals and Slots

```
// main.cpp (Commit f9e49e66)
ClimateModel model;
ClimateEcuTwin twin;
QObject::connect(&model, &ClimateModel::temperatureChanged,
                 &twin, &ClimateEcuTwin::setTemperature);
```

The explicit dependency between `ClimateModel` and `ClimateEcuTwin` is gone. `ClimateModel` now depends on a [mediator class](https://en.wikipedia.org/wiki/Mediator%5Fpattern?ref=smallstepsystems.com) generated by Qt's meta-object compiler `moc`. The mediator class depends on `ClimateEcuTwin`.

```
// ClimateModel.cpp
void ClimateModel::setTemperature(int temperature)
{
    m_temperature = temperature;
    emit temperatureChanged(m_temperature);
}
```

*ClimateModel.cpp* does **not** include *ClimateEcuTwin.h*. Executing `model.setTemperature(15)` triggers the call sequence specified by the signal-slot connection.

```
model.setTemperature(15)
model.temperatureChanged(15)
mediator.temperatureChanged(15)
twin.setTemperature(15)
// Send temperature 15 to Climate ECU.
```

The use of Qt's signals and slots adds a little overhead. The classes `ClimateModel` and `ClimateEcuTwin` must both inherit `QObject` and define the `Q_OBJECT` macro. They must declare `temperatureChanged` as a signal and `setTemperature` as a slot.

This little overhead is easily offset by eliminating the cyclic dependency and by turning the tight coupling between the business logic and machine adapter components into a loose coupling. We can deploy both components independently. Next, we rewrite Qt signals and slots in pure C++.

## Loose Coupling through Pure C++ Signals and Slots

```
// main.cpp (Commit 7d3f443f)
ClimateModel model;
ClimateEcuTwin twin;
model.temperatureChanged = [&twin](int temperature)
{
    twin.setTemperature(temperature);
};
```

The left-hand side of the assignment corresponds to the first line of the Qt `connect` statement (the signal part). The lambda function on the right-hand side of the assignment corresponds to the second line of the Qt `connect` statement (the slot part).

```
// ClimateModel.cpp
void ClimateModel::setTemperature(int temperature)
{
    m_temperature = temperature;
    temperatureChanged(m_temperature);
}
```

The implementation of `setTemperature` is identical to the Qt solution. It only drops the Qt-specific `emit` statement. *ClimateModel.cpp* does **not** include *ClimateEcuTwin.h* either. Executing `model.setTemperature(14)` triggers the following call sequence.

```
model.setTemperature(14)
model.temperatureChanged(14)
twin.setTemperature(14)
// Send temperature 14 to Climate ECU.
```

An explicit mediator is not required. The function `main()` knows both the sender `model` and the receiver `twin` of the signal-slot connection. Assigning the lambda function to the member variable `model.temperatureChanged` connects the signal function `model.temperatureChange`d to the slot function `twin.setTemperature`. In general, a class holding references to the sender and to the receiver would define the signal-slot connection - instead of `main()`.

```
// ClimateModel.h
#include <functional>

class ClimateModel
{
public:
    std::function<void(int)> temperatureChanged;
    ...
```

The member variable `temperatureChanged` holds a [function object](https://en.cppreference.com/w/cpp/utility/functional/function?ref=smallstepsystems.com) with the signature `void function(int)`. In general, we define signal function objects with N arguments as follows.

```
std::function<void(arg-type-1, arg-type-2, ..., arg-type-N)> signalFunction;
```

For 0 arguments, the declaration is

```
std::function<void()> signalFunction;
```

The pure C++ implementation of signals and slots doesn't need the overhead of the Qt solution. This is pretty nifty.