Skip to content

Implementing Qt Signals and Slots in Pure C++

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. 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. 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 with std::function and lambda functions.

You find the example code in the subdirectory BlogPosts/SignalsSlotsInCpp of my GitHub repository embeddeduse. 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 (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 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.temperatureChanged 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 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.