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 Q_OBJECT 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

A spinbox in Plastique theme, stolen from the Qt docs 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.

Alternative designs

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 NetworkHub or 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

The Boost logo, stolen from the boost docs 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 that. __VA_ARGS__ is C99... I'm thinking a typelist template where you can shift on args with &lt;&lt; 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 ASYNC_EMIT_EVENT 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 boost:: namespace anyway.) See the Boost.Signals FAQ.

Miscellaneous complaints

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 int into a 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.

Last words

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.)

Page generated on Tue Oct 24 00:33:38 2017