As many long running projects, Qt too over the years has accumulated some APIs that in hindsight are deemed unsafe or sub-optimal. For example, Qt by default implicitly converts const char* to QString. While that usually only incurs a runtime overhead, maybe encoding problems, but also admittedly less cluttered code, there’s other APIs that can backfire in more subtle ways. One such API is doing a “context-less connect”.
Signals and Slots are a core principle of Qt that make it super easy to connect one object to another and keep a certain separation of concerns. The typical syntax to establish an connection is:
connect(sender, &Foo::somethingHappened, receiver, &Bar::doStuff);
This connects the signal somethingHappened in our sender of Type Foo to the member function doStuff in our receiver (context object) of type Bar. Whenever sender “emits” somethingHappened, doStuff on receiver will be called. The neat part about Qt connections is that when receiver gets destroyed, the connection is severed automatically. However, you can not just connect a signal to a member function but also use a lambda:
connect(job, &WallpaperFinder::wallpaperFound, [this](const QString &path) {
m_wallpapers << path;
});
In our hypothetical wallpaper selector when the “job” that goes looking for wallpapers found one, it emits a signal and tells us the path of the file, so we can show it to the user. Now what happens when the user closes the dialog (which then gets destroyed) before the job has finished? Well… job still emits the signal which then results in our lambda being called. And then we try to access m_wallpapers on this which is long gone. Boom!
The fix is easy: provide a “context object”, too, just like you would with pointer to member function:
connect(job, &WallpaperFinder::wallpaperFound, this, [this](const QString &path) {
m_wallpapers << path;
});
If this gets destroyed, the connection is severed, our lambda will no longer be called and all is well. The receiver object also decides what thread the slot is called, i.e. it will be called in the thread the receivers “lives in”. Context-less connections are always of DirectConnection type. Since Qt 6.7 you can actually enforce the use of a context object by defining QT_NO_CONTEXTLESS_CONNECT.
add_definitions(-DQT_NO_CONTEXTLESS_CONNECT)
It requires you think about the lifetime of your objects more and make a conscious decision about what your context object is. It also removes one thing to look out for in code review. I started adding this option to a couple KDE repositories to improve the quality of our code and I encourage you to do that, too! It probably comes to no surprise that in general, the bigger and older a repository, the higher the probability of it using non-ideal code.
What context object to use?
Of course, the situation is not always as simple as our example above. Here’s a few tips and tricks:
- Look at the lambda captures. If you capture this, chances are, you want this as your context object.
- If you capture a single object, perhaps you want it as your context object rather than this. If you use the sender, too, that’s fine, capture them both:
connect(job, &Job::finished, manager, [job, manager] {
manager->report(job);
});
When the sender gets destroyed, evidently the connection is useless and will be severed. - You can try to avoid capturing this by doing an init capture, i.e.
[foo = m_foo] { ... } - Perhaps you don’t even need a lambda and can just connect to the method directly. Lambdas are so ubiquitous that you tend to forget you could just replace
connect(job, &Job::finished, [timer] {
timer->start();
});
with:connect(job, &Job::finished, timer, &QTimer::start); - At last, you may also use the sender as context object.
- A context object must be a QObject, though. If you don’t have one, you’ll have to find another way. For instance, QObject::connect returns a QMetaObject::Connection object that you can store in a member variable and then disconnect when appropriate, like in your destructor.
- For connections where it doesn’t really matter qApp can also be an option.
I’m a huge fan of QT_ENABLE_STRICT_MODE_UP_TO that lets you turn on most strictness features in a single shot. The biggest hurdle of rolling that out more widespread in KDE repositories is actually the Java-style iterators. Qt hates them, many use them, particularly for mutating a container, and imho they’re much more pleasing to look at than STL algorithms. If you start a new project, however, do consider setting your baseline to be as strict as it can be!
Would it make sense to extend Clazy by a checker to mark Java-style iterators?