Code interlude – signals and slots
In software systems, there is often the need to couple different objects. Ideally, this coupling should be loose, that is, not dependent on the system's compile-time configuration. This is especially obvious when you consider user interfaces, for example, a button press might adjust the contents of a text widget, or cause something to appear or disappear. Many systems use events for this purpose; components offering data encapsulate that data in an event, and an event loop (or more recently, an event listener) catches the event and performs some action. This is known as event-driven programming or the event model.
Qt offers signals and slots as the interface it uses to manage events. Like an event, the sending component generates a signal—in Qt parlance, the object emits a signal, which is an occurrence of an event—which recipient objects may execute a slot for the purpose. Qt objects might emit more than one signal, and signals might carry arguments; in addition, multiple Qt objects can have slots connected to the same signal, making it easy to arrange one-to-many notifications.
Qt provides a macro, connect
, that lets you connect signals to slots. Equally important is the fact that if no object is interested in a signal, it can be safely ignored and no slots will be connected to the signal. Equally important, if no slots are connected to a signal, it will simply be ignored. Any object that inherits from QObject
, Qt's base class for objects, can emit signals or provide slots for connection to signals. Under the hood, Qt provides extensions to C++ syntax to declare signals and slots.
A simple example will help make this clear. The classic example you find in the Qt documentation is an excellent one, and we'll use it again here, with some extension. Imagine that you have the need for a counter, that is, a container that holds an integer. In C++, you might write something like the following block of code:
class Counter { public: Counter() { m_value = 0; } int value() const { return m_value; } void setValue(int value); private: int m_value; };
The Counter
class has a single private member, m_value
, bearing its value. Clients can invoke value
to obtain the counter's value, or set its value by invoking setValue
with a new value.
In Qt, we can write the class this way, using signals and slots:
#include <QObject> class Counter : public QObject { Q_OBJECT public: Counter(QObject *parent = 0) : QObject(parent), m_value(0) { } int value() const { return m_value; } public slots: void setValue(int value); void increment(); void decrement(); signals: void valueChanged(int newValue); private: int m_value; };
This Counter
class inherits from QObject
the base class for all Qt objects. To make all the functionality of QObject
(such as the signal-slot mechanism) available, subclasses of QObject
must include the declaration Q_OBJECT
as the first element of their definition; this macro expands to the Qt code, implementing the subclass-specific glue necessary for the Qt object and signal-slot mechanism. The constructor remains the same, initializing our private member to 0. Similarly, the accessor method value remains the same, returning the current value for the counter.
An object's slots must be public and are declared using the Qt extension to C++ public slots. This code defines three slots: a setValue
slot, which accepts a new value for the counter, and the increment and decrement slots, which increment and decrement the value of the counter. Slots might take arguments, but must not return them; the communication between a signal and its slots is one way: initiating with the signal and terminating with the slot(s) connected to the signal.
The counter offers a single signal. Signals are also declared using a Qt extension to C++ signals. A Counter
object emits the valueChanged
signal a single argument, which is the new value of the counter. A signal is a function signature, not a method; Qt's extensions to C++ use the type signature of signals and slots to ensure type safety between signal-slot connections, a key advantage signals and slots have over other decoupled messaging schemes.
As developers, it's our responsibility to implement each slot in our class with whatever application logic makes sense. The Counter
class's slots look like this:
void Counter::setValue(int newValue) { if (newValue != m_value) { m_value = newValue; emit valueChanged(newValue); } } void Counter::increment() { setValue(value() + 1); } void Counter::decrement() { setValue(value() – 1); }
We use the implementation of the setValue
slot as a method, which is what all slots are at their heart. The setValue
slot takes a new value and assigns the new value to the private member variable of Counter
if they aren't the same. Then, the signal emits the valueChanged
signal, using the Qt extension emit
, which triggers an invocation to the slots connected to the signal.
Tip
This is a common pattern for signals that handle object properties: testing the property to be set for equality with the new value and only assigning and emitting a signal if the values are unequal.
If we had a button, say, QPushButton
, we could connect its clicked signal to the increment
or decrement
slot so that a click on the button increments or decrements the counter. I'd do this using the QObject::connect
method, like this:
QPushButton* button = new QPushButton(tr("Increment"), this); Counter* counter = new Counter(this); QObject::connect(button, SIGNAL(clicked(void)), counter, SLOT(increment(void));
We first create the QPushButton
and Counter
objects. The QPushButton
constructor takes a string, the label for the button, which we denote to be the Increment
string or its localized counterpart.
Why do we pass this to each constructor? Qt provides a parent-child memory management between the QObject
objects and their descendants, easing cleanup when you're done using an object. When you free an object, Qt also frees any children of the parent object so that you don't have to. The parent-child relationship is set at construction time; I signal to tell the constructors that when the object invoking this code is freed, PushButton
and counter
can be freed as well. (Of course, the invoking method must also be a subclass of QObject
for this to work.)
Next, I call QObject::connect
, passing first the source object and the signal to be connected and then the receiver object and the slot to which the signal should be sent. The types of the signal and slot must match and the signals and slots must be wrapped in the SIGNAL
and SLOT
macros, respectively.
Signals can be connected to signals too, and when this happens, the signals are chained and trigger any slots connected to the downstream signals. For example, I can write:
Counter a, b; QObject::QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
This connects the b
counter with the a
counter so that any changes in value to the a
counter also change the value of the b
counter.
There's also disconnect
, which breaks the connection between a signal and a particular slot. Invoking disconnect is similar to invoking connect:
disconnect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
This disconnects the connection we made in the previous example.
Signals and slots are used throughout Qt, both for user interface elements and to handle asynchronous operations such as the presence of data on network sockets, HTTP transaction results, and so forth. Under the hood, signals and slots are very efficient, boiling down to function dispatch operations, so you shouldn't hesitate to use the abstraction in your own designs. Qt provides a special build tool, the meta-object compiler, which compiles the extensions to C++ and is required for signals and slots. It generates the additional code necessary to implement the mechanism.