C++11 Lambda表达式:成员变量捕获陷阱

41

考虑以下代码:

#include <memory>
#include <iostream>

class A
{
public:
    A(int data) : data_(data)
    { std::cout << "A(" << data_ << ")" << std::endl; }
    ~A() { std::cout << "~A()" << std::endl; }
    void a() { std::cout << data_ << std::endl; }
private:
    int data_;
};

class B
{
public:
    B(): a_(new A(13)) { std::cout << "B()" << std::endl; }
    ~B() { std::cout << "~B()" << std::endl; }
    std::function<void()> getf()
    {
        return [=]() { a_->a(); };
    }
private:
    std::shared_ptr<A> a_;
};

int main()
{
    std::function<void()> f;
    {
        B b;
        f = b.getf();
    }
    f();
    return 0;
}

在这里看起来我是按值捕获了 a_ 的共享指针,但是当我在 Linux 上运行它(GCC 4.6.1),会打印出这个:

A(13)
B()
~B()
~A()
0

显然,0是错误的,因为A已经被销毁了。看起来this实际上被捕获并用于查找this->a_。当我将捕获列表从[=]更改为[=,a_]时,我的怀疑得到了确认。然后正确的输出被打印出来,对象的生命周期也符合预期:

A(13)
B()
~B()
13
~A()

问题:

这种行为是由标准规定的,实现定义的,还是未定义的?或者我疯了,它完全不同?


1
我认为这符合标准,因为a_通常会解析为this->a_,除非你明确告诉它复制a_。 - Daniel
2
对我来说,这似乎完全合法 - 范围内唯一的变量是 this。如果指针只是神奇地成为成员解引用,那将是令人惊讶的!不过这是一个好问题,并且对于孩子们不要轻率地使用懒惰捕获 [=]/[&] 是一个很好的警告。 - Kerrek SB
1个回答

43

这种行为符合标准规范吗?

是的。捕获成员变量总是通过捕获 this 来完成的;这也是访问成员变量的唯一方式。在成员函数的作用域中,a_ 等同于 (*this).a_。在 Lambda 中也是如此。

因此,如果你使用 this(隐式或显式地),那么你必须确保对象在 lambda 实例存在期间仍然有效。

如果你想按值捕获它,你必须明确指定:

std::function<void()> getf()
{
    auto varA = a_;
    return [=]() { varA->a(); };
}

如果您需要一个规范报价:

lambda表达式的复合语句产生函数调用运算符的函数体 (8.4),但为了名称查找(3.4),确定this的类型和值(9.3.2)以及将引用非静态类成员的id表达式转换为使用(*this)的类成员访问表达式(9.3.1), 复合语句在lambda表达式的上下文中被考虑。


8
在我看来,Lambda函数中最可怕的陷阱之一。我只希望所有编译器最终都会为此添加一个明显的警告。 - Martin Ba
6
@Dani 但是将一个 shared_ptr 返回到一个成员变量并不是错误。当我在将现有代码转换时,我遇到了这种潜在的常见错误。我认为应该发出警告(至少在 -Wextra 标志下),并且可以通过显式地使用 this-> 访问成员,或者将其添加到捕获列表中来消除警告。 - Alex B
12
对我来说,这里的问题不在于那个让我自己踢到自己脚的可恶的老C++,而是Lambda中的“按值捕获”默认捕获了this而不是_a。在我看来,这值得一个可选的警告,无论Lambda是否被返回。如果警告促使你明确地写出(*this)._a,那么很明显就是this被捕获了。编译器会为其他比这更不危险、更不微妙不正确的事情发出警告。 - Steve Jessop
4
@Nicol:在函数调用中内部使用这个lambda函数是“始终安全”的说法是错误的。不论是在内部还是外部使用它,它仍然会捕获this(在lambda函数源代码中没有出现过),而不是_a。你可能会在不离开作用域的情况下重置或修改智能指针,如果你期望_a被按值捕获,那么你的代码仍然是错误的。可选警告的整个目的是识别程序员预期有点不合理的地方,尽管不正确,但期望[=]捕获_a并不完全是不合理的。 - Steve Jessop
10
@Nicol: 我不想“惩罚”任何人,我认为可选的警告是有道理的。你不必启用它,所以我觉得你这么热情地反对其他人使用它很令人惊讶。你觉得[=]显然捕获的是this而不是_a,因为你完全熟悉该领域的标准。那很好,但编译器为语言更微妙的部分提供警告也是常见的。 - Steve Jessop
显示剩余9条评论

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