【问题标题】:What is the lifespan of the captured stack allocated variables in lambda functions used as slots?用作插槽的 lambda 函数中捕获的堆栈分配变量的寿命是多少?
【发布时间】:2017-08-11 21:59:23
【问题描述】:

我需要帮助了解 lambda 函数的工作方式,以防止在使用它们时发生内存泄漏。更具体地说,我想知道foo在以下情况下何时被销毁:

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;
    });
}

使用[&amp;]的情况如何?

【问题讨论】:

  • 正如所写,t 是按值捕获的,lambda 存储指针的副本。没关系。如果改为通过引用捕获,则一旦函数返回,lambda 将以悬空引用结束,并且一旦调用该 lambda,程序就会表现出未定义的行为。
  • @IgorTandetnik,foo 呢?
  • 同样的事情 - 如果按值捕获则很好,如果按引用捕获则悬空引用。
  • 一个lambda只是编译器自动生成的某个类的一个对象; foo 成为该类的数据成员。粗略地说,你最终会得到 class L { QTimer *t; bool foo; void operator()(); }; 。 lambda 表达式创建该类的一个实例——它是一个临时的,它将在分号处被销毁(当然,连同它的成员变量一起)。 connect 抓取一个临时副本,并将其存放在某个地方。当QTimer 对象被销毁,或者当您通过disconnect() 断开连接时,它将被销毁
  • 事实上,现在我想起来了,您的程序可能会通过在对象的生命周期结束后访问对象而表现出未定义的行为。 delete t; 可能导致正在执行的 lambda 对象的破坏;此时,它的foo 数据成员也将被销毁。之后使用是一个问题。您可能想使用QObject::deleteLater - 它就是为这种情况而发明的。

标签: c++ qt lambda qt5


【解决方案1】:

求值的 lambda 表达式是一个仿函数实例。函子是具有operator() 的对象。捕获的变量是该函子对象的成员。 它们的生命周期不会根据它们的类型而改变。因此,无论您捕获引用还是值,它们的生命周期都是相同的。 您的工作是确保引用有效 - 即它们引用的对象没有被破坏。

仿函数的生命周期与连接的生命周期相同。连接,因此函子,将持续到:

  1. QObject::connect()的返回值上调用QObject::disconnect(),或者

  2. QObject 的生命结束并调用其析构函数。

考虑到上述情况,局部变量按引用捕获的唯一有效用途是当局​​部变量的寿命超过连接时。一些有效的例子是:

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` 
}

或者:

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
}

您问题中的代码过于复杂。无需手动管理此类计时器:

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

这里的重要部分是提供对象上下文(this)作为singleShot 的第二个参数。这确保了this 必须比函子更长寿。反之,函子会在this 被销毁之前被销毁。

假设您真的想实例化一个新的瞬态计时器,在连接到此类信号的插槽中删除信号的源对象是未定义的行为。您必须将删除推迟到事件循环:

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();
  });
}

tfoo 都被复制到仿函数对象中。 lambda 表达式是一种符号速记 - 您可以自己显式编写它:

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));
}

由于 lambda 实例只是一个对象,您当然可以将它分配给一个变量,如果多个信号需要连接到同一个 lambda,您当然可以摆脱代码重复:

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

lambda 的类型是唯一的、不可言说的,也称为不透明的类型。你不能从字面上提到它——语言中没有这样做的机制。您只能通过decltype 参考它。这里decltype中的不应该被命名的人。 C++ 人只是在一个哈利波特的笑话中工作,不管他们是否有意。否则我不会被说服。

【讨论】:

  • 我不能强调这个解释是多么有用和彻底。自从您提供答案以来已经一个月了,每次我回去阅读它时,我都会发现越来越多的关于 lambdas 的信息。现在我只缺少一件事。有没有办法将两个或多个信号连接到完全相同的 lambda 以避免代码重复,或者在这种情况下手动创建类更好(如 $OpaqueType 所示) 并将其作为参数传递给所有相应的connect 语句?
  • 感谢您的快速响应!我有这个想法。但是,它具有以下副作用:当this 指向的对象停止存在时,lambda 也会停止存在。因此信号断开。相反,当 lambda 在connect 语句中定义时,它的寿命比创建它的对象长。我认为问题下方@Igor Tandetnik 的评论解释了原因。更具体地说,它的这一部分:“connect 获取该临时文件的副本,并将其存储在某个地方”。当 lambda 在 connect 语句之外定义时,这显然不会发生。
  • connect 管理 lambda 的生命周期,尽管您可能需要给它一个右值,而不是左值。
  • 我没有得到这个值。请解释一下。
  • 你没有 - 怎么办?显示一些重现该内容的代码。
【解决方案2】:

捕获变量的 lambda 本质上是一个未命名的数据结构,捕获的变量成为该结构的成员。该数据结构被认为是可调用的,因为它提供了一个接受指定参数的operator()

如果变量是按值捕获的,则 lambda 成员会保存这些值。只要 lambda 确实存在这些值,就像您的情况一样,作为捕获变量的副本 - 无论最初捕获的变量是否继续存在。

通过引用捕获的变量有点不同。在这种情况下,lambda 包含对变量(例如堆栈变量)的引用,如果被引用的变量不再存在,它将成为悬空引用。类似地,如果捕获的值是一个指针,并且它指向的内容不复存在。在这些情况下,使用捕获的引用或取消引用指针会产生未定义的行为。

【讨论】:

  • 感谢您的回复,彼得!我的问题是关于将 lambda 函数用作 Qt 中的插槽的情况。请注意,在问题下方的 cmets 中,Igor 已经给出了一个答案,可以准确地解释这一点。
  • “只要 lambda 存在,这些值就存在”这很明显,但是 lambda 的寿命是多少呢?这就是问题所在。
  • @WindyFields 问题是关于 lambda 中捕获的变量的生命周期(如示例中的 foo)而不是 lambda 本身的生命周期。在示例中,lambda 是一个临时的,它的生命周期是到创建它的语句的末尾。但就像任何临时(可以复制的任何类型)一样,它可以被复制 - 例如,connect() 可以将它存储在另一个变量中。副本的生命周期就是它被复制到的变量的生命周期。
  • @windyfields - “在 Qt 中用作插槽”与如何在 Qt 代码中复制或引用 lambda 相关 - lambda 的生命周期和它在 Qt 代码中的任何副本的生命周期是什么影响程序的行为。这不像 Qt 可以在调用函数时改变变量(包括 lambdas)的生命周期,但它可以创建它们的副本并控制副本的生命周期。您链接到的回复中也没有任何内容与我所说的相矛盾。
  • @scopchanov - 重要的是您有信息可以提供帮助。正如您所说,我没有解决在 Qt 中使用 lambda 作为插槽的问题,但 Igor 解决了(我只描述了基本的 C++ 方面,这将支持这一点)。如果 Igor 选择发布答案,我同意 Igor 解决 Qt 方面的问题值得称赞。
猜你喜欢
  • 1970-01-01
  • 2014-08-19
  • 2012-01-18
  • 2015-08-19
  • 1970-01-01
  • 2016-04-14
  • 2014-07-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多