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.

9 thoughts on “Implementing Qt Signals and Slots in Pure C++”

    1. Hi Mike,

      Yes, I know CopperSpice. Other frameworks with signals and slots are the header-only library KDBindings and the Boost signals and slots library. Replacing one framework (Qt) by another framework was not my intention. I wanted to understand how difficult it is to write signals and slots in pure C++. I think it’s simple enough to decouple components or layers.

      Cheers,
      Burkhard

  1. But Qt signals/slots handle cross-thread signaling, multiple slots, and disappearing slots.

    1. Hi Mark,

      I don’t know what “multiple slots” and “disappearing slots” are.

      Regarding cross-thread or queued signal-slot connections. I see two options.

      First, the receiving class (ClimateEcuTwin in the example) guards critical sections, say, with a mutex.

      Second, the receiving class has a message queue. The lambda function adds a function object to the receiver’s queue, instead of executing the function object directly. Adding and removing objects from the queue is in a critical section guarded appropriately. The message queue is similar to QThread with its event loop.

      Cheers,
      Burkhard

      1. Hi Burkhard,

        I mean you can connect many slots to a signal, and if a slot is deleted the signal will know. Basically just saying those things are important to any library you want to use, and make implementation of a GOOD signal/slot mechanism non-trivial. No real criticism of the post meant.

        Regards,
        Mark

  2. and Qt signals and slots are part of the QMetaObject framework allowing introspection and manipulation at runtime.

    1. Hi Tom,

      I know 😉 My intention was not to write a replacement framework for Qt’s signal and slots. I just wanted a simple and pure C++ implementation of signals and slots for projects, where Qt is not allowed.

      Cheers,
      Burkhard

  3. Thank you for the post. This is essentially a callback, which has been in use for decades — it is a staple in C and in many other languages. It’s nice that `std::function` enables the use of bound functions/methods and lambda expressions as well as the usual direct function pointer.

    Personally, my favorite use of the Qt signal/slot mechanism is for multi-threaded applications where the copied messages are queued up and prevent the need for handling of my own mutexes. It drastically simplifies my design of multi-threaded applications, even cli-based ones.

    1. Hi Mike,

      Yes, signals and slots are callbacks conceptually – just a lot nicer.

      I also like to use Qt signals and slots for multi-threaded applications, where Qt’s event loops take over the synchronisation. Very cute!

      The premise of the post was that we cannot or must not use Qt. I have encountered several projects where Qt Commercial was used for the GUI and was forbidden for the non-GUI backend. This “saved” customers a couple of developer licenses. I think that these extra licenses would easily pay for themselves, but some customers are hard to convince.

      Cheers,
      Burkhard

Leave a Reply

Your email address will not be published. Required fields are marked *