blog

Photo of glasses in front of screen by Kevin Ku on Unsplash

Qt 5 and C++11: Lambdas Are Your Friend

by

Since Qt 5 was released I had been putting off upgrading to Qt 5 on a project I have been working on. Even minor ports like this one, from Qt 4.7 to Qt 5 (that’s right…skipped 4.8) for some reason are never as easy as implied. “Just change the include and link paths,” they said. “It’ll just build,” they said. Psht, yeah right. Not falling for it again.

Well, I finally made the jump and, after many years of working in Qt, I feel like C++ and Qt are working together now instead of Qt just being something that helps you develop better C++. I believe the signal/slot mechanism has found its soul mate in C++11 lambda functions.

What’s this signal/slot thingy?

If you don’t work in Qt you probably don’t care anyway but the fundamental communication mechanism between objects in the Qt framework is defined by signals (events that can be emitted) and slots (handlers for events).
As a quick introduction, consider the following example (assume the setupUi method creates three QPushButton objects and a QTextEdit):

class AMainWindow : public QWidget, public Ui::AMainWindow
{
Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
      // create the widget interface (add the buttons and text box)
      this->setupUi(this);
      // connect the signal/slots
      connect(pushButton, SIGNAL(clicked()), this, SLOT(SetTextOne()));
      connect(pushButton_2, SIGNAL(clicked()), this, SLOT(SetTextTwo()));
      connect(pushButton_3, SIGNAL(clicked()), this, SLOT(SetTextThree()));
}
public slots:
   void SetTextOne(void)
   {
      textEdit->setText("bonjour");
   }
   void SetTextTwo(void)
   {
      textEdit->setText("comment allez vous");
   }
   void SetTextThree(void)
   {
      textEdit->setText("pas trop mal, et vous?");
   }
};

We’ve connected the click signals for each of the buttons to the three methods defined there.
SIGNAL and SLOT used in the connect method calls are macros that resolve the names of the functions that are being connected. For our purposes, let’s just assume it’s magic.

So, what was wrong with the signal/slot mechanism?

Well, nothing was exactly broken with this, right? It works…I guess.

The SIGNAL/SLOT macro “magic” I talked about earlier isn’t really that magical. Both of those macros actually resolve to a string. Problem #1: it uses strings to resolve connections…at runtime. So if you happen to have a slot that takes a string and the signal declaration takes an int…you don’t know it until you run your application. Unless you’re used to JavaScript development, this probably catches you off guard.

Problem #2: Why on earth do I have to define three methods that do basically the same exact thing?

Cleanup Pre-Qt5

Before Qt5 and C++11, we could the QSignalMapper class to do something like this:

class AMainWindow : public QWidget, public Ui::AMainWindow</span>
{
   Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
       // create the widget interface
       this->setupUi(this);
       // use the QSignalMapper to pass custom string for each button
       // to a single slot
       QSignalMapper* mapper = new QSignalMapper();
       connect(mapper, SIGNAL(mapped(QString)), this, SLOT(SetText(QString)));
       connect(pushButton, SIGNAL(clicked()), mapper, SLOT(map()));
       mapper->setMapping(pushButton, "bonjour");
       connect(pushButton_2, SIGNAL(clicked()), mapper, SLOT(map()));
       mapper->setMapping(pushButton_2, "comment allez vous");
       connect(pushButton_3, SIGNAL(clicked()), mapper, SLOT(map()));
       mapper->setMapping(pushButton_3, "pas trop mal, et vous?");
   }
public slots:
   void SetString(QString text)
   {
       textEdit->setText(text);
   }
};

That’s not bad. We’ve eliminated the three slot functions that do basically the same thing with different text and replaced it with a single function. You and I both know that this feels a bit hackish though. QSignalMapper is like a patch over a genuine problem.

Adding C++11 Lambda Functions

If you been following the evolution of C++, you might know about lambda functions and expressions. If you aren’t familiar with them you can find tons of information by searching, but the short answer is that they are basically in-line, unnamed functions. The general format is: capture { body }; where capture specifies which symbols visible in the scope where the function is declared will be visible to the body of the lambda, parameters is the list of parameters passed into the lambda, and body is the definition of the function.

What does that mean for Qt?

The most important thing to consider is that they can be used as function pointers…for slots. We can connect a slot like this:

connect(<pointer to source object>, <pointer to signal>,
        <pointer to slot function>);

Firstly, notice that the we can now pass in actual pointers to signals and slots instead of just using the SIGNAL and SLOT macros (which you can still use if you want). That means compile time checking of connections. No more running the program and finding out you used an int for your slot but the signal passed an int.

Secondly, a lambda is basically just a function pointer. Consider how this changes our example:

class AMainWindow : public QWidget, public Ui::AMainWindow
{
   Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
       // create the widget interface
       this->setupUi(this);
       // connect the signal to lambda (using [=] to capture variables
       // used in the body by value)
       connect(pushButton, &QPushButton::clicked,
               [=]() { this->SetString("bonjour"); });
       connect(pushButton_2, &QPushButton::clicked,
               [=]() { this->SetString("comment allez vous"); });
       connect(pushButton_3, &QPushButton::clicked,
               [=]() { this->SetString("pas trop mal, et vous?"); });
   }
public slots:
   void SetString(QString text)
   {
       textEdit->setText(text);
   }
};

* Note: If you are using Clang on Mac, you may need to add “CONFIG += c++11” to your .pro file to enable the C++11 features, including lambda functions.

Compare this to our fix above using QSignalMapper. It’s simply cleaner and easier to follow. However, we don’t need to stop there. Since our slot function is so simple and really the important stuff (the string we are setting) is in the lambda, there is no reason to even have it.

class AMainWindow : public QWidget, public Ui::AMainWindow
{
   Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
       // create the widget interface
       this->setupUi(this);
       // connect the signal to lambda (using [=] to capture variables
       // used in the body by value)
       connect(pushButton, &QPushButton::clicked,
               [=]() { textEdit->setText("bonjour"); });
       connect(pushButton_2, &QPushButton::clicked,
               [=]() { textEdit->setText("comment allez vous"); });
       connect(pushButton_3, &QPushButton::clicked,
               [=]() { textEdit->setText("pas trop mal, et vous?"); });
   }
};

This is a far more elegant solution than our original class with three different slots.

Caveats

Of course, not everything is perfect. There are a couple of things you have to keep in mind when using pointers to functions and lambdas as slots. Firstly, it’s a little more complicated in that you have to specify the full type of the slot class (if you don’t use a lambda) but less ambiguity never hurt anyone.

The two bigger issues are:

  1. default arguments are not supported when connecting by function pointer and
  2. slots created using lambdas are not automatically disconnected when the ‘receiver’ is destroyed.

The second isn’t necessarily a major problem since the fact that you are using lambda functions suggest you aren’t planning on connecting/disconnecting often anyway (though it is still possible manually).
The first, however, can be annoying. Consider a connection like this:

connect(fromObject, &ASomeClass::someSignal, 
        toObject, &AAnotherClass::slotHandler);

where the signal and slot are defined as:

void ASomeClass::someSignal(bool arg = true);
void AAnotherClass::slotHandler() { cout << “handled”; }

Even though the arguments match when considering the default argument, the connect method will throw a compile-time error. Of course, a simple fix is using a lambda like this and just ignoring the signal argument:

connect(object, &ASomeClass::someSignal,
        [=](bool arg) { toObject->slotHandler(); });

Problem solved.

+ more