Emitting a custom signal using lambdas
The remove task is straightforward to implement, but we'll study some new concepts along the way. The Task
has to notify its owner and parent (MainWindow
) that the removeTaskButton
QPushButton
has been clicked. We'll implement this by defining a custom signal removed
in Task.h
files:
class Task : public QWidget { ... public slots: void rename(); signals: void removed(Task* task); ... };
Like we did for the slots, we have to add the Qt keyword signals in our header. Since a signal
is used only to notify another class, the public
keyword is not needed (it even raises a compilation error). A signal
is simply a notification sent to the receiver (the connected slot
); it implies that there is no function body for the removed(Task* task)
function. We added the task
parameter to allow the receiver to know which task asked to be removed. The next step is to emit the removed
signal upon the removeButton
click. This is done in the Task.cpp
file:
Task::Task(const QString& name, QWidget *parent) : QWidget(parent), ui(new Ui::Task) { ui->setupUi(this); ... connect(ui->removeButton, &QPushButton::clicked, [this] { emit removed(this); }); }
This code excerpt shows a very interesting feature of C++11: lambdas
. In our example, the lambda
is the following part:
[this] { emit removed(this); });
What we did here is to connect the clicked signal to an anonymous inline function, a lambda
. Qt allows signal relaying by connecting a signal to another signal if their signatures match. It's not the case here; the clicked
signal has no parameter and the removed
signal needs a Task*
. A lambda
avoids the declaration of a verbose slot
in Task
. Qt 5 accepts a lambda
instead of a slot in a connect
, and both syntaxes can be used.
Our lambda
executes a single line of code: emit removed(this)
. Emit
is a Qt macro that will immediately trigger the connected slot
with what we passed in a parameter. As we said earlier, removed(Task* this)
has no function body, its purpose is to notify the registered slot of an event.
Lambdas are a great addition to C++. They offer a very practical way of defining short functions in your code. Technically, a lambda
is the construction of a closure capable of capturing variables in its scope. The full syntax goes like this:
[ capture-list ] ( params ) -> ret { body }
Let’s study each part of this statement:
capture-list
: This defines what variables will be visible inside thelambda
scope.params
: This is the function parameters type list that can be passed to thelambda
scope. There are no parameters in our case, We might have written[this] () { ... }
, but C++11 lets us skip the parentheses altogether.ret
: This is the return type of thelambda
function. Just likeparams
, this parameter can be omitted if the return type isvoid
.body
: This is obviously your code body where you have access to yourcapture-list
, andparams
, and which must return a variable with a typeret
.
In our example, we captured the this
pointer to be able to:
- Have a reference on the
removed()
function, which is a part of theTask
class. If we did not capturethis
, the compiler would have shoutederror: 'this' was not captured for this lambda function emit removed(this);
. - Pass
this
to theremoved
signal; the caller needs to know which task triggeredremoved
.
The capture-list
relies on standard C++ semantics: capture variables by copy or by reference. Let us say that we wanted to print a log of the constructor parameter name
and we capture it by reference in our lambda
:
connect(ui->removeButton, &QPushButton::clicked, [this, &name] { qDebug() << "Trying to remove" << name; this->emit removed(this); });
This code will compile fine. Unfortunately, the runtime will crash with a dazzling segmentation fault when we try to remove a Task
. What happened? As we said, our lambda
is an anonymous function that will be executed when the clicked()
signal has been emitted. We captured the name
reference, but this reference may be -
and is -
invalid once we get out of Task
constructor (more precisely, from the caller scope). The qDebug()
function will then try to display an unreachable code and crash.
You really want to be careful with what you capture and the context in which your lambda will be executed. In this example, the segmentation fault can be amended by capturing the name
by copy:
connect(ui->removeButton, &QPushButton::clicked, [this, name] { qDebug() << "Trying to remove" << name; this->emit removed(this); });
Tip
C++ Tip
- You can capture by copy or reference all variables that are reachable in the function where you define your lambda with the syntax [
=
] and [&
]. - The
this
variable is a special case of the capture list. You cannot capture it by reference[&this]
and the compiler will warn you if you are in this situation:[=, this]
. Don't do this. Kittens will die.
Our lambda
is passed directly as a parameter to the connect function. In other words, the lambda
is variable. This has many consequences: we can call it, assign it, and return it. To illustrate a "fully formed" lambda
, we can define one that returns a formatted version of the task name. The sole purpose of this snippet is to investigate the lambda
function's machinery. Don't include the following code in your todo
app, your colleagues might call you something like a "functional zealot":
connect(ui->removeButton, &QPushButton::clicked, [this, name] { qDebug() << "Trying to remove" << [] (const QString& taskName) -> QString { return "-------- " + taskName.toUpper(); }(name); this->emit removed(this); });
Here we did a tricky thing. We called qDebug()
; inside this call we defined a lambda
which is immediately executed. Let's analyze it:
[]
: We performed no capture. Thelambda
does not depend on the enclosing function.(const Qstring& taskName)
: When this lambda is called, it will expect aQString
to work on.-> QString
: The returned value of the lambda will be aQString
.return "------- " + taskName.toUpper()
: the body of ourlambda
. We return a concatenation of a string and the uppercase version of the parametertaskName
. As you can see, string manipulation becomes a lot easier with Qt.(name)
: Here comes the catch. Now that thelambda
function is defined, we can call it passing thename
parameter. In a single instruction, we define it and call it. TheQDebug()
function will simply print the result.
The real benefit of this lambda
will emerge if we are able to assign it to a variable and call it multiple times. C++ is statically typed, so we must provide the type of our lambda
variable. In the language specification, a lambda
type cannot be explicitly defined. We'll see soon how we can do it with C++11. For now, let's finish our remove feature.
Task now emits the removed()
signal. This signal has to be consumed by MainWindow
:
// in MainWindow.h public slots: void addTask(); void removeTask(Task* task); // In MainWindow.cpp void MainWindow::addTask() { ... if (ok && !name.isEmpty()) { qDebug() << "Adding new task"; Task* task = new Task(name); connect(task, &Task::removed, this, &MainWindow::removeTask); ... } } void MainWindow::removeTask(Task* task) { mTasks.removeOne(task); ui->tasksLayout->removeWidget(task); task->setParent(0); delete task; }
The MainWindow::removeTask()
must match the signal signature. The connection is made when the task is created. The interesting part comes in the implementation of MainWindow::removeTask()
.
The task is first removed from the mTasks
vector. It is then removed from tasksLayout
. Here, tasksLayout
releases its ownership of task
(that is, tasksLayout
ceases to be the task
class's parent).
So far so good. The next two lines are interesting. The ownership transfer does not completely release the task
class ownership. If we commented these lines, here is how removeTask()
will look:
void MainWindow::removeTask(Task* task) { mTasks.removeOne(task); ui->tasksLayout->removeWidget(task); // task->setParent(0); // delete task; }
If you add a log message in Task
destructor and execute the program, this log message will be displayed. Nonetheless, the Qt documentation tells us in Qlayout::removeWidget
part: The ownership of a widget remains the same as when it was added.
Instead, what really happens is that the task
class's parent becomes centralWidget
, the tasksLayout
class's parent. We want Qt to forget everything about task
, that's why we call task->setParent(0)
. We can then safely delete it and call it a day.