Implementing Qt Signals and Slots in Pure C++
source link: https://embeddeduse.com/2022/08/28/implementing-qt-signals-and-slots-in-pure-c/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 C++ 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.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 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK