Why I dislike Qt signals/slots
(Originally posted on Sunday, February 19th, 2012.)
I've created over a dozen small projects using Qt by now. Most of the time I think I might as well make use of Qt's signals/slots system -- I mean it's already there. And this almost always turns out to be a mistake, in terms of programming effort, duplicated and fragile code, and what it is possible to do with the system.
Let me quickly summarize Qt signals/slots for completeness. Qt uses a code
generator (the Meta-Object Compiler or
moc) to implement flexible
signals/slots. Classes can mark themselves as moc'able with the
macro (and must inherit
QObject), then indicate that some functions in the
class are slots, and some are signals. Slots have declarations and definitions
just like normal functions; signals are essentially just a function prototype,
and have no definitions (the moc provides them).
Any signal can be connected to a slot with a matching signature, and indeed a signal can be connected to another signal. It's possible for the slot/second signal to have fewer arguments than the first. The signals are processed during Qt's event loop and, if the target is in another thread, the arguments are automatically serialized and sent to that thread's event queue. This is very nice to have, as you can imagine.
The problem with connections
The issue I have with Qt signals and slots is how they are connected. Some
class must know about both the signal and the slot (or second signal) and call
QObject::connect(). For GUI widgets this works quite well: you connect the
valueChanged() signal of the spin box to the setEnabled() slot of the "Apply"
button, so that when the user changes a setting they can now apply their
changes. Or you connect a signal to your own private slot, do a little
processing, and when you're done you emit a simplified signal of your own.
Seems like a good system.
The catch is that some class must know about both the sender and the receiver. Essentially this means signals/slots cannot go more than one level up or down in your object hierarchy. So when you start using them in a non-GUI context, and you need to communicate information to a great-uncle (so to speak) or some other object far removed, you need to duplicate the signal at every object it passes through! (Fortunately you don't need slots, just signals, and the top-level class can connect signals of two of its children togther.) This is a lot of duplicated information. I have had multiple signals which had to be duplicated at least two or three times in this way to get to where they needed to go.
This makes the code hard to change: you don't want to add a parameter to the
signal, or remove the signal, or add another one, because it will involve
digging through all these other classes and changing their signals and
connect() calls. And heaven forbid you want to refactor some of those
classes, or move them around in the object hierarchy.
Of course there are alternative designs one could employ: since signals/slots
are runtime-checked and not compile-time checked, you could pass around
QObjects that are interested in being registered for some event, or walk
through a hierarchy and add connections in a separate pass. And I'm sure there
are many other ways. But I haven't found any that appeal to me yet.
The problem is simply that someone has to know about both the sender and the
receiver. It's quite easy to remove this requirement if you build your own
event system. Just have a singleton
EventSystem class which can register
Observer objects for various concrete subclasses of
Event. Anyone can grab
the singleton and register an observer object (or more likely a method, it's
easy enough to wrap a method in a functor object). Anyone can grab the
singleton and emit an event. There are no ties whatsoever between the sender
and the receiver. They are completely disconnected.
This may sound hopelessly chaotic, but it's not, really (as long as you have a good way for observers to automatically get unregistered when their originating object is destructed). I've occasionally needed to turn on tracing to see how many objects register themselves to listen for a particular event type, but such debugging is rare. Mostly you register a method, or emit an event, and forget about it. The amount of code required is much less.
Aha, you say, but you've replaced Qt's nice object-based signals/slots system
with a global set of events which is sure to become unmanagable! Well, I
usually have a singleton event "hub" per namespace (like
QtGUIHub). I mark the events that are private to the namespace, and also the
events that are intended for a wider audience. Normally it works out to a
handful of events per dozen classes. If there are more, probably the coupling
between areas of the system is too high.
(You could try to make Qt signals/slots global: one singleton, with signals. But as far as I could tell you'd have to duplicate every signal in the class that was actually emitting it, because you can't emit another class's signals. And someone would have to connect all those signals together.)
To me, events should be global. Maybe this is my problem, that I try to treat Qt signals/slots as events, and I should learn how to use them properly. But these are my conclusions so far.
What boost can do
I should add that writing your own event system takes a lot of work, especially if you want observer lifetime management, multithreading support, and so on. I tend to use boost instead, which can supply all this with very minimal amounts of code. Specifically, I use the Boost.Smart Ptr, Boost.Signals, and Boost.Bind libraries (links for 1.48, use Google if they're broken). In an event hub you can define an event like this (and this means an event class is not required):
boost::signal<void (std::string)> PacketReceived;
Then you can add method observers and emit events with these macros:
#define METHOD_OBSERVER1(hub, event, fullyQualifiedMethod) \ hub::getInstance()->event.connect( \ boost::bind(fullyQualifiedMethod, this, _1)) #define EMIT_EVENT1(hub, event, a1) \ hub::getInstance()->event(a1)
(Yes, you need parameter-numbered versions of these macros. I'm working on
__VA_ARGS__ is C99... I'm thinking a typelist template where you can
shift on args with
<< shows promise....)
Lifetime management of observers is performed automatically with shared
pointers: any class which has a method observer must inherit the class
boost::signals::trackable, which I usually typedef to something else.
If you want multithreading support, just add another
macro which binds all the arguments to form a zero-arg functor, then register
the event as an observer for a zero-arg signal. Another thread can come
along and "fire the observers" for this signal (passing it no arguments), thus
calling all the queued up events with the arguments they were supposed to have.
Then clear all the "observers" unless you want duplicate events. Wrap
everything in sufficient mutexes (or use Boost.Signals2? that never worked for
me) and you're good to go. Role-reversal: events are really observers in
another guise. Very sly, I know. Took me a while to come up with that one.
Also, you can use both Qt signals/slots and boost signals in the same project.
Qt unfortunately uses
#define signals protected somewhere in its headers, and
"signals" is the name of the boost library. But you can convince
boost::signals to rename itself to work around Qt and then everything works
just fine. (The interesting classes are put right into the
the Boost.Signals FAQ.
I have a few other gripes about Qt's signals/slots. First of all, if you have a slot which takes as an argument a class you've defined, and you want to connect a signal from a different namespace to that slot -- well, the only way I can usually get this to compile and run is to fully qualify the class in both the signal and the slot (since Qt's string-matching argument checks will otherwise fail). And of course the same goes for all other signals/slots connected to these. There might be a better solution for this, but I think Qt doesn't really use namespaces so they don't really notice issues like this.
There's no type conversion in Qt signals/slots. If you want to turn an
double, or an
int into a
uint32, you need to create a one-line
slot just to do so. With just about any C++ signals implementation (whether a
custom-built one, or boost's), you get C++ type-conversion.
It's often really difficult to figure out the source of a signal. Sure,
QSignalMapper can bind objects to numbers or strings, but only for signals
that originally had zero arguments! At least twice I've had to emit two
separate signals to get across all the information I needed: e.g., a socket
ID (its index in a list of sockets), and the newly received data. The best
solution, of course, was to not use so many signals/slots, have the socket
store its own index and use that.
This is the final complaint I have about signals/slots: they tend to encourage "bottom-up" programming. For example, a socket will receive some data and emit a signal; its parent will catch that and figure out which socket ID, then re-emit that; the general networking class will catch that and pass it on to a different child, the packet parser, and so on. I've used the word "catch" deliberately here. It's like noticing a change in data at the lowest level and then throwing exceptions all the time to report this back to the caller. Using signals/slots messes with the normal function calling conventions so badly you can't always figure out what your own code is doing.
When you use an event hub class, you tend to only put important events in there and not bother with small everyday communications. Which means that these small communications will be ordinary function calls and hence much easier to follow. Object-message-passing might be fine for Objective C (I don't know the language), but C++ should be written as C++ was intended to be written ....
I guess maybe that's the primary benefit for me of not using Qt signals/slots. I end up with fewer events. Maybe that's all I need.
Finally, I must say that Qt's object lifetime management system has caused me
at least as much grief as the signals/slots system. Again, it might be fine for
widgets, but for everything else, it's very hard to tell when your objects are
deleted. If ever. The number of bugs it's caused ... like a socket handshaker
that was supposed to have been deleted but was still slurping up all my
network traffic ... look, that's the subject of another rant almost as long as
this one, which I don't have time to write. But its conclusion is, always use
boost::shared_ptr instead of Qt's object lifetime management whenever you can
get away with it. Then you know precisely which objects are keeping another
object alive, and you don't have to worry about a parent object. Just my
(slightly) informed opinion so far.
Don't get me wrong, I enjoy using Qt for GUI design. I think its layouts and HTML support and container-widgets make it a very powerful library for GUIs. I just let that convince me that Qt would therefore be good for everything else, too. But it isn't, necessarily. Just be aware of the other tools that are out there and how they can be used. (And checking what Boost has is often a good idea.)