lambda表达式中的'this'是在何时被捕获的?

24

我的类中有一个函数,它定义了一个 lambda 并将其存储在本地静态变量中:

class A
{
public:
    void call_print()
    {
        static auto const print_func = [this] {
            print();
        };

        print_func();
    };

    virtual void print()
    {
        std::cout << "A::print()\n";
    }
};

class B : public A
{
public:
    virtual void print() override
    {
        std::cout << "B::print()\n";
    }
};

我也执行以下测试:

int main()
{
    A a;
    B b;

    a.call_print();
    b.call_print();
}

我期望被打印出来的内容是:

(实时示例)

A::print()
B::print()

但是真正得到的是:

A::print()
A::print()

(每次打印时,也会显示相同的对象地址)

我怀疑这是由于 this 捕获引起的。我以为它会捕获调用时的 this 值,但似乎捕获的是 lambda 定义时的值。

有人能解释一下 lambda 捕获的语义吗?它们实际上是什么时候提供给函数的?对于所有捕获类型都是这样的吗,还是 this 是一个特殊情况?去掉 static 可以解决问题,但在我的生产代码中,我实际上是将 lambda 存储在稍微重一些的对象中,该对象表示稍后要插入信号的插槽。

6个回答

36

这与lambda捕获语义无关,这只是static的工作原理。

static函数作用域变量仅被初始化一次。在整个程序中只有一个这样的对象。它将在第一次调用该函数时进行初始化(更具体地说,是在执行static语句的第一次调用)。因此,用于初始化static变量的表达式也只会被调用一次。

如果一个static函数作用域变量是基于函数参数(如this)的数据进行初始化的,那么它将只获取该函数的第一次调用的参数。

您的代码创建了一个单独的lambda表达式,而不是在每次函数调用时都创建不同的lambda表达式。

您似乎想要的行为不是函数局部的static变量,而是对象成员变量。因此,只需将std::function对象放置在类本身中,并且如果为空,则让call_print进行初始化即可。


11
值得一提的是,这种编程风格实际上非常危险,因为如果a被删除,未来对call_print的调用将会引发未定义行为,并很可能导致崩溃。 - Xirema
2
@void.pointer:一个 lambda 在生成后的某个时刻如何能够捕获 任何东西?Lambda 并不是魔法;它们只是一种声明类型的花式方式。 - Nicol Bolas
1
@void.pointer说:“我想要Lambda表达式的容器是静态的,但捕获内容不是。” 这就像 struct S { int i; }; static const S s { 42 }; ,每次都期望s.i不同。 - Oktalist
1
我意识到在引用和按值捕获的上下文中,我的问题并没有意义。我想我之所以认为this捕获会是一个特殊情况,可能是因为调用实际上可以发生在类本身之外,所以后来无法捕获this。感谢您提供示例进行澄清。 - void.pointer
1
@void.pointer:这就是为什么每个人都认为你的问题在于理解“static”的行为方式。因为Lambda表达式不可能像你所想象的那样神奇地捕获。 - Nicol Bolas
显示剩余8条评论

12

当第一次调用封闭函数A::call_print()时,lambda表达式被实例化。由于第一次在A对象上调用它,因此该对象的this被捕获。如果你改变了调用顺序,你会看到不同的结果:

b.call_print();
a.call_print();

输出:

B::print()
B::print()

这与函数内静态对象的初始化语义有关,而不是lambda捕获的语义。


8

静态局部变量在第一次声明执行时被初始化。

在这种情况下,您需要:

  1. 创建一个 lambda 表达式,捕获 &A 作为 this,并调用 A.print();
  2. 将该 lambda 表达式分配给 print_func
  3. 通过 print_func 调用该 lambda 表达式
  4. 再次调用该 lambda 表达式。

捕获的值总是在 lambda 表达式创建时捕获 - 在这种情况下是在第一次调用 call_print 时。


4

我认为问题不在于捕获,而在于 static 关键字。 试着将您的代码想象成这样:

class A
{
public:
    void call_print()
    {
        static A* ptr = this;

        ptr->print();
    };

    virtual void print()
    {
        std::cout << "A::print()\n";
    }
};

class B : public A
{
public:
    virtual void print() override
    {
        std::cout << "B::print()\n";
    }
};

没有lambda表达式的情况下,这基本上是一样的。

如果您查看代码,会很清楚地发现,调用a.call_print();ptr初始化为指向对象a的指针,然后进一步使用它。


3

这个问题并不是关于lambda行为的,而是关于函数中静态变量的行为。

当代码第一次流经变量时,它会使用那个时刻可用的任何变量来创建它。

例如:

#include <iostream>

int foo(int v)
{
  static int value = v;
  return v;
};

int main()
{
  std::cout << foo(10) << std::endl;
  std::cout << foo(11) << std::endl;
}

期望的结果:

10
10

因为它相当于:

foo::value = 10;
std::cout << foo::value << std::endl;
// foo::value = 11; (does not happen)
std::cout << foo::value << std::endl;

2

实际上,捕获值是在定义lambda时设置的,而不是在调用时设置的。因为您将静态变量设置为定义lambda的表达式,所以这仅在第一次调用函数call_print时发生(由于控制静态变量的规则)。因此,此后所有的call_print调用实际上都会得到相同的lambda,即其this被设置为&a的那个。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接