scopchanov
scopchanov

Reputation: 8419

What is the lifespan of the captured stack allocated variables in lambda functions used as slots?

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?

Upvotes: 4

Views: 1862

Answers (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.

Upvotes: 2

Peter
Peter

Reputation: 36617

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.

Upvotes: 2

Related Questions