in Code, HowTo

Using C++11 Lambdas As Qt Slots

Qt Library

Qt Framework

I have an old codebase I started writing using the Qt 3.x framework—a little while before Qt4 was released. It’s still alive! I still work on it, keeping up-to-date with Qt and C++ as much as possible, and I still ship the product. Over the years I have moved the codebase along through Qt4 to Qt5, and on to compilers supporting C++11. One of the things I’ve sometimes found a little excessive is declaring slots for things that are super short and won’t be reused.

Here’s a simplified example from my older code that changes the title when a project is “dirty” (needs to be saved):

(Aside: If you are wondering about my naming convention, I developed a style when using Qt early on (c. 2000) where signals are named signalFoo() and slots are named slotFoo(). This way I know at a glance what the intent is. If I’m about to modify a slot function I might take an extra minute to look around since most IDEs can’t tell syntactically where it’s used in a SLOT() macro. In this case you have to search for it textually.)

Thanks to C++11 lambdas and Qt’s ongoing evolution, these short slots can be replaced by a more succinct syntax. This avoids having to declare a method in your class declaration and shortens your implementation code. Both desirable goals!

Let’s take a look.

Getting rid of SIGNAL() and SLOT() macros

The first really nice thing about Qt5 is that we can move away from the classic SIGNAL() and SLOT() macros to using method pointers.

This does several things for us:

  • relieves us of having to lookup and fill in the parameters for the methods
  • shortens your code
  • IDEs can identify the methods used in a connect call when you search for uses of a method
  • allows implicit conversion of arguments
  • catches problems at compile time rather than runtime

This last one is the most important. If the two methods you are trying to connect are mismatched it simply won’t compile. I can say with authority that problems found at compile time can save you many, many hours of debugging!

So if we apply this idea to our example, the connect() call becomes:

Already an improvement!

What are these lambda things anyways?

A lambda is essentially a fancy name for an anonymous function. These are typically short and can be useful for things like this slot mechanism where the code may not be re-usable, or for callbacks.

The C++11 specification for a lambda function looks like this:

[capture](parameters) -> return_type { function_body }

Looks simple, but when you dig into the details, they can be rather complicated.

The capture part defines how variables in the current scope are captured for the function_body to use. Put another way, it defines what variables are made available to the lambda function body from the current scope.

The one we’ll use here is [=] which says to capture all variables from the local scope by value. You can also capture nothing ([]), capture all variables in the scope by reference ([&]*note), capture just the members of the class ([this]), or capture individual variables ([foo, &bar]) – or some combination.

It also takes optional parameters which is how we’ll pass signal parameters to the lambda (see below).

return_type and function_body are standard C++.

If we apply this to our example, we get something like this:

Here we connect our signal to an anonymous function which has access to the variables in the current scope. We leave out the return type so void is inferred.

(This is a very quick intro to lambdas. You can find a much more in-depth description at cppreference.com and in this post.)

Our final result

Here’s our result if we put all that together. We got rid of the extra declaration in the header and the extra function in the implementation. Much nicer.

What about the arguments?

As I mentioned earlier, lambdas can also take arguments, so you can pass the parameters from the signal into the lambda function. For example, I recently ran into an issue where I needed to accept a QRectF, adjust it, and send it on again. I did it like this:

You’ll note that I am also using mRatio and mView from the enclosing class. Because we captured using [=], the body of the lambda function has access to variables in the enclosing scope.

Because I’m only using the members of this, I could have captured using [this] instead:

I’m not aware of any advantages of one over the other – except that maybe reducing the scope the lambda function body has access to may be desirable. If you have any opinions on this, please leave a comment!

Caveat

Depending on how you work, using lambdas may make testing more difficult. Having separate methods you can call in testing might be more desirable.

It’s also tempting to use lambdas in lots of places. It’s pretty powerful. I think, however, that restricting it to simple things like the above—replacing short one-use slots, adjusting data before passing it along, or for simple callbacks—will be the most maintainable in the long run. Until we work with them and live with the resulting code for a few years we won’t know…

Hope that helps someone out there on the information superhighway!

* Lambdas Using References

As Ronaldo Nazarea points out in the comments:

You have to be careful though about capture by reference (or pointers if by value). If the slot is not run inside the scope of the definition, you may end up with references to invalid locations caused by out of scope variables.

Leave a Reply for Andy Maloney Cancel Reply

Write a Comment

Comment

  1. Please note in the text that you should use decltype() keyword or manually name the type. Otherwise great post!

    • Dimitry:

      Could you explain a bit more what you’re referring to? Are you talking about specifying the return_type of the lambda? In the signal/slot case (the focus of this post) the return type is void. As I mentioned, this is inferred if you leave it out.

      • Hello Andy and thank you for replying. What I mean (maybe it’s me being a bad reader) is that it’s currently unclear from your post that 2nd argument of this connect can’t be a signal of an object (like it was before, at least how it seemed to me when I used SIGNAL() syntax). It must be a signal of a class (e,g. QPushButton::clicked())

        • Dmitry:

          So is it the change to member function pointers from SIGNAL() (which is const char *) that confused you? If it’s that then I can add something in there about it. (I’m still curious where decltype() comes in!)

          The instantiated object is the first argument to connect(). The 2nd argument is the signal of the object – &QPushButton::clicked is the address of the method (PointerToMemberFunction in the docs).

          In these examples, I’m using this form of QObject::connect.

          Thanks Dmitry. Obviously there’s some confusion and I’d like to clear it up if possible!

          • Ok, I need to expalin my thought train. I think “neat”, copy-paste your example (last one, with mNavView) into Qt Creator, and it would print me an error instead of building “Not a class, namespace or enumeration”.
            I think the difference between my case and yours is that I’m trying to connect to QTableView that is in public: of some widget.
            And here decltype comes in (not neccessarily). I prefer my code to be as explicit as it gets without becoming bloated, so I’d always write the type name itself. But if someone’s lazy, they could use decltype() instead of that 🙂
            So to clear up the confusion (IMHO) you should change &mNavView:: to decltype(mNavView):: or to QViewClassName::

          • Ah I think I see the misunderstanding now.

            In the last example, mNavView is the instantiated object, myNavView is the class. I am using the class in the second argument – “&myNavView::signalNavBoxChange”, not “&mNavView::” as you quote.

            (“myNavView” is not the actual name I use. I don’t use “Q*” for any of my own classes – I believe only Qt classes should use that – but I can change it to “&ViewClassName”.)

            If this is indeed the confusion I will change that to clear it up.

            Thanks!

  2. You have to be careful though about capture by reference (or pointers if by value). If the slot is not run inside the scope of the definition, you may end up with references to invalid locations caused by out of scope variables.