4

I need help understanding the way lambda functions work in order to prevent memory leaks when using them. More specifically, I would like to know when foo will be destroyed in the following case:

void MainWindow::onButtonClicked()
{
    QTimer *t(new QTimer(this));
    bool foo = false;

    t->setSingleShot(true);
    t->setInterval(1000);
    t->start();

    connect(t, &QTimer::timeout, [=](){
        delete t;
        qDebug() << foo;
    });
}

What about the case when [&] is used?

scopchanov
  • 7,966
  • 10
  • 40
  • 68
  • 2
    As written, `t` is captured by value, lambda stores a copy of the pointer. That's fine. If you capture by reference instead, lambda will end up with a dangling reference once the function returns, and the program will exhibit undefined behavior as soon as that lambda is invoked. – Igor Tandetnik Aug 11 '17 at 22:02
  • @IgorTandetnik, what about foo? – scopchanov Aug 11 '17 at 22:03
  • 1
    Same thing - fine if captured by value, dangling reference if captured by reference. – Igor Tandetnik Aug 11 '17 at 22:04
  • @IgorTandetnik, you're right. I've corrected it by adding initialization to `foo`. So at which point of the execution `foo` will be destroyed? – scopchanov Aug 11 '17 at 22:07
  • Which `foo`? When you capture by value, there are two of them. One is a local variable in `onButtonClicked()`, and as any local variable, it is destroyed when it goes out of scope at the closing brace. The other is a data member of the lambda object, a copy of the former `foo`. In fact, `connect` will copy the lambda itself (into whatever internal data structure Qt uses to represent signal-slot connections), at which point `foo` will get copied yet again. – Igor Tandetnik Aug 11 '17 at 22:11
  • @IgorTandetnik, I understand that foo gets copied and the first one (defined in `onButtonClicked()` in this case) is destroyed when the execution of `onButtonClicked()` finishes. But the point I get confused is when trying to figure out when the copy is destroyed (if it is indeed destroyed). The things get more tricky when I try to understand how it will behave if the copy is manipulated inside the lambda and the slot is called multiple times and not just once, as in this case. – scopchanov Aug 11 '17 at 22:19
  • 3
    A lambda is just an object of some class automatically generated by the compiler; `foo` becomes a data member of that class. Roughly, you end up with `class L { QTimer *t; bool foo; void operator()(); };` . The lambda expression creates an instance of that class - it's a temporary, it will be destroyed at the semicolon (together with its member variables, of course). `connect` grabs a copy that temporary, and stashes it somewhere. It'll be destroyed when the `QTimer` object is destroyed, or when you sever the connection via `disconnect()` – Igor Tandetnik Aug 11 '17 at 22:21
  • @IgorTandetnik, I understand that, but to be honest I have a hard time figuring out how this object works together with the Qt's signal-slot mechanism. As you said connect copies the lambda itself and this is the point where I am lost. – scopchanov Aug 11 '17 at 22:26
  • 2
    In fact, now that I think of it, your program would probably exhibit undefined behavior by way of accessing an object after its lifetime has ended. `delete t;` likely causes the destruction of the very lambda object being executed; at this point, its `foo` data member would also be destroyed. Using is after that is a problem. You may want to use `QObject::deleteLater` - it's invented for exactly this scenario. – Igor Tandetnik Aug 11 '17 at 22:26
  • I'm not sure I quite grasp the nature of your confusion. Qt stores, for each signal of each object, a list of callable objects representing connections from that signal. When the signal is emitted, Qt walks that list and calls each callable on it. That's pretty much all there is to it. – Igor Tandetnik Aug 11 '17 at 22:31
  • @IgorTandetnik, The compiler will create an object for the lambda and foo gets copied by value, becoming its data member. Each time the slot is called, this very object and its copy of foo will be accessed, no matter how many times the slot is called. Is that right, or each time the slot is called a new instance of the lambda is created? – scopchanov Aug 11 '17 at 22:40
  • @scopchanov, the same lambda will be called each time the signal is emitted. You can leverage that to store some state in a mutable lambda for example. . . – Mike Aug 11 '17 at 22:45
  • @Mike, if the same lambda is called, then when will `foo` be destroyed? When the signal is disconnected? – scopchanov Aug 11 '17 at 22:52
  • Which part of *"It'll be destroyed when the `QTimer` object is destroyed, or when you sever the connection via `disconnect()`"* did you find unclear? – Igor Tandetnik Aug 11 '17 at 22:58
  • @IgorTandetnik, I've just read the addition to the "A lambda is just an object of some class...". I think it is exactly what I have been missing to understand "...how this object works together with the Qt's signal-slot mechanism". Please add this as an answer and I will accept it. And keep in mind, that it takes a while to be notified about the edits, so (probably) I am not a complete moron. ;) – scopchanov Aug 11 '17 at 23:37

2 Answers2

2

A lambda that captures variables is essentially an unnamed data structure, and the captured variables become members of that structure. That data structure is considered callable because it provides an operator() that accepts specified arguments.

If variables are captured by value, members of the lambda hold those values. Those values exist as long as the lambda does, as in your case, as copies of the captured variables - regardless of whether the originally captured variables continue to exist.

It's a bit different for variables captured by reference. In that case, the lambda contains a reference to a variable (e.g. a stack variable), and that becomes a dangling reference if the referred variable ceases to exist. Similarly, if the captured value is a pointer, and what it points at ceases to exist. In these cases, usage of the captured reference or dereferencing the pointer give undefined behaviour.

Peter
  • 35,646
  • 4
  • 32
  • 74
  • Thanks for the reply, Peter! My question was about the case when a lambda function is used as a slot in Qt. Please note, that in the comments below the question there is already an answer by Igor which explains exactly that. – scopchanov Aug 12 '17 at 00:40
  • "Those values exist as long as the lambda does" that is obvious, but what is the lifespan of lambda then? That was the question. – WindyFields Aug 12 '17 at 06:42
  • 1
    @WindyFields The question was about lifetime of variables captured in the lambda, (like `foo` in the example) not lifetime of the lambda itself. In the example, the lambda is a temporary, and its lifetime is to the end of the statement that creates it. But like any temporary (of any type that can be copied) it can be copied - for example, `connect()` may store it in another variable. The lifetime of the copy is then the lieftime of the variable it is copied to. – Peter Aug 12 '17 at 07:10
  • 1
    @windyfields - The "used as slots in Qt" is related to how the lambda is copied or otherwise referred to within Qt code - the lifetime of the lambda AND lifetime of any copies of it within Qt code are what affect behaviour of the program. It's not like Qt can change the lifetime of variables (including lambdas) in calling functions, but it can create copies of them and control life cycles of the copies. There is also nothing in that response you link to which contradicts what I'm saying. – Peter Aug 12 '17 at 08:38
  • @WindyFields, Peter is right. It is not about capturing variables in lambda functions in general, as it becames quite obvious (at least for me) just by reading this: http://en.cppreference.com/w/cpp/language/lambda. So "used as slots in Qt" is very important part of the question and can't be ignored. Luckily, Igor Tandetnik already explained this aspect in the comments under the question and I hope that he will add this as an answer, because this is exactly the thing I've been missing. – scopchanov Aug 12 '17 at 10:30
  • 1
    @scopchanov - the important thing is you've got information to help. As you say, I did not address the use of a lambda as a slot in Qt, but Igor did (I only described basic C++ aspects, which would underpin that). I agree Igor deserves credit for addressing the Qt aspects, if he chooses post an answer. – Peter Aug 12 '17 at 11:59
  • Peter, I apreciate your help. As a matter of fact your answer and the comments underneath helped me to understand it even better. That is why I upvoted them all (I would do this several times if I could). However, I think it is fair and also helpfult to future readers with the same problem, to have Igor's explainaition as an accepted answer. I am grateful to both of you for the time spent and the efforts. – scopchanov Aug 12 '17 at 12:53
2

An evaluated lambda expression is a functor instance. A functor is an object with operator(). The captured variables are members of that functor object. Their lifetime doesn't change based on their type. Thus, whether you capture references or values, their lifetime is the same. It's your job to ensure that the references are valid - i.e. that the objects they reference haven't been destroyed.

The functor's lifetime is the same as the connection's lifetime. The connection, and thus the functor, will last until either:

  1. QObject::disconnect() is invoked on the return value of QObject::connect(), or

  2. The QObject's life ends and its destructor is invoked.

Taking the above into account, the only valid use of local variable capture-by-reference is when the local variables outlive the connection. Some valid examples would be:

void test1() {
  int a = 5;
  QObject b;
  QObject:connect(&b, &QObject::destroyed, [&a]{ qDebug() << a; });
  // a outlives the connection - per C++ semantics `b` is destroyed before `a` 
}

Or:

int main(int argc, char **argv) {
  QObject * focusObject = {};
  QApplication app(argc, argv);
  QObject * connect(&app, &QGuiApplication::focusObjectChanged,
                    [&](QObject * obj){ focusObject = obj; });
  //...
  return app.exec();  // focusObject outlives the connection too
}

The code in your question is needlessly complex. There's no need to manage such timers manually:

void MainWindow::onButtonClicked() {
  bool foo = {};
  QTimer::singleShot(1000, this, [this, foo]{ qDebug() << foo; });
}

The important part here is to provide the object context (this) as the 2nd argument of singleShot. This ensures that this must outlive the functor. Conversely, the functor will be destroyed before this is destroyed.

Assuming that you really wanted to instantiate a new transient timer, it is undefined behavior to delete the signal's source object in a slot connected to such a signal. You must defer the deletion to the event loop instead:

void MainWindow::onButtonClicked()
{
  auto t = new QTimer(this);
  bool foo = {};

  t->setSingleShot(true);
  t->setInterval(1000);
  t->start();

  connect(t, &QTimer::timeout, [=](){
    qDebug() << foo;
    t->deleteLater();
  });
}

Both t and foo are copied into the functor object. The lambda expression is a notational shorthand - you could write it explicitly yourself:

class $OpaqueType {
  QTimer * const t;
  bool const foo;
public:
  $OpaqueType(QTimer * t, bool foo) :
    t(t), foo(foo) {}
  void operator()() {
    qDebug() << foo;
    t->deleteLater();
  }
};

void MainWindow::onButtonClicked() {
  //...
  connect(t, &QTimer::timeout, $OpaqueType(t, foo));
}

Since a lambda instance is just an object, you can certainly assign it to a variable and get rid of code duplication should more than one signal need to connect to same lambda:

auto f = [&]{ /* code */ };
connect(o, &Class::signal1, this, f);
connect(p, &Class::signal2, this, f);

The type of the lambda is a unique, unutterable, also called opaque, type. You cannot mention it literally - there's no mechanism in the language to do so. You can refer to it via decltype only. Here decltype is the he in he who shall not be named. C++ people just worked in a Harry Potter joke, whether they meant to or not. I won't be convinced otherwise.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • 1
    I just can't stress enough how helpful and thorough this explanation is. It's been one month already since you provided the answer and I discover more and more about lambdas each time I go back reading it. For now I am missing just one thing. Is there a way to connect two or more signals to the exact same lambda in order to avoid code duplication or is it better in that case to go with the manual creation of the class (as shown for _$OpaqueType_) and pass it as an argument to all of the corresponding `connect` statements? – scopchanov Oct 02 '17 at 17:45
  • Thank you for the fast response! I've got the idea. It has the following side effect, though: when the object to which `this` points stops existing, so does the lambda. Thus the signal gets disconnected. In contrast, when the lambda is defined within the `connect` statement it outlives the object in which it is created. I think the comment by @Igor Tandetnik below the question explains why. More specifically this part of it: _"connect grabs a copy (of) that temporary, and stashes it somewhere"_. This apparently does not happen when the lambda is defined outside of the `connect` statement. – scopchanov Oct 03 '17 at 16:09
  • `connect` manages the lambda's lifetime, although perhaps you need to give it an rvalue, not an lvalue. – Kuba hasn't forgotten Monica Oct 18 '17 at 16:21
  • I didn't get this with the values. Explain, please. – scopchanov Oct 18 '17 at 20:01
  • You didn't - how? Show some code that reproduces that. – Kuba hasn't forgotten Monica Oct 19 '17 at 15:04
  • Sorry, I've said that in an ambiguous way. I've meant: I did'nt understand what the "give it" part of "to give it an rvalue, not an lvalue" means. I would be grateful if you elaborate more on that, perhaps with a code example. I am not sure though if it's not worth to open a new question, because as far as the current one is regarded, I've recieved enough info to be able to write test code, watch the behavior, explain the results to myself and even use lambdas as slots in an actual application. I just don't know yet how to formulate it, since I didn't get the meaning of the mentioned sentence. – scopchanov Oct 20 '17 at 00:45