Lambda失去捕获的价值

时间:2019-05-26 11:42:57

标签: c++ lambda

我不明白为什么捕获的值会丢失。我知道它与LambdaWrapper的对象超出范围或复制有关。但是到底发生了什么?如果LambdaWrapper(100)离开了“添加”中的作用域,并且丢失了对__value的引用,那么为什么LambdaWrapper(300)没有相同的内容。

#include <iostream>
#include <vector>
#include <functional>
using namespace std;

class LambdaWrapper {
public:
    LambdaWrapper(double new_value): __value (new_value) {
        cout << "constructed with " << __value << endl;
        __func = [this](){ cout << __value << endl;};
    }
    void call() const { __func(); }
private:
    double __value;
    function<void(void)> __func;
};

class LambdaContainer {
public:
    void Add(double value) {
        LambdaWrapper w(value); //out of scope
        parts.push_back(w);
    }

    void Add(LambdaWrapper w) // passed as value
    {
        parts.push_back(w);
    }

    void call() const {
        for (const auto& part : parts)
                part.call();
    }
private:
    vector<LambdaWrapper> parts;
};

int main() {
    LambdaContainer c;
    c.Add(100);

    LambdaWrapper w(200);
    c.Add(w);

    c.Add( LambdaWrapper(300) ); //LambdaWrapper(300) will out of scope too

    cout << "==============" << endl;
    c.call();

    return 0;
}

输出:

constructed with 100
constructed with 200
constructed with 300
==============
6.95168e-308 <<< WHY?
200
300

3 个答案:

答案 0 :(得分:2)

如果lambda已经是这样的包装器,为什么需要这样做呢?要保留捕获?你在做相反的事情。

您在Add(double)方法中创建的闭包捕获this的值,该值指向调用该方法的对象。当方法超出范围时,该对象“死亡”。该指针的值保持不正确,它是指向本地或时间对象的悬挂指针。由于这种设计,其他对象也会发生同样的情况。

LambdaWrapper(const LambdaWrapper &obj) {
        __value = obj.__value;
        __func = [this](){cout << __value << endl;};
    }

该构造函数有效,因为它会创建具有新this值的新lambda并将其持久化。 Lambda只不过是带有指针字段(大概为__this)的类实例的语法糖,指针字段存储值this并包含void operator() () {cout << __this->__value << endl;};

Add(double)的任何调用都将导致指针和UB悬空,对临时对象的调用也将导致UB,因为该对象不会与const引用绑定(无论如何只能在本地使用),所以它是一个悬空的指针也。该方法按值接受包装器,这将导致复制的另一步骤。

该设计次优,因为每次创建新对象时。如果仅捕获值,则不需要复制构造函数。

PS。为了说明lambda封闭的性质,GCC甚至遇到了一个bug \ flaw,在那里lambda的成员可以从外部访问,因为它们不是私有的。

答案 1 :(得分:1)

请务必注意@Peter的评论。

如果您真的想要一个解决方案,请自己定义LambdaWrapper的副本目录,以便它可以捕获源对象的this

class LambdaWrapper
{
public:
    LambdaWrapper(double new_value): __value (new_value)
    {
        cout << "constructed with " << __value << endl;
        __func = [this](){ cout << __value << endl;};
    }
    LambdaWrapper(const LambdaWrapper &obj) {
        __value = obj.__value;
        __func = [this](){cout << __value << endl;};
    }
    void call() const { __func(); }
private:
    double __value;
    function<void(void)> __func;
};

答案 2 :(得分:1)

我想这个问题是问发生了什么,而不是“没事”,所以在这种情况下(通常)gdb是您的朋友,修改程序以在构造过程中打印此地址,在内部__func和对象在容器中的实际地址,我们看到: (地址各不相同,但距离和概念应保持不变)

# During constructors and captured value:
0x7fffffffda80 <- LambdaWrapper(100)
0x7fffffffdb00 <- LambdaWrapper(200)
0x7fffffffdb60 <- LambdaWrapper(300)
# Actual address of data stored in the container:
0x6170c0 <- LambdaWrapper(100)
0x6170e8 <- LambdaWrapper(200)
0x617110 <- LambdaWrapper(300)

存在巨大的价值差异,这是因为创建发生在堆栈上,而向量在上使用new分配数据。< / p>

通过gdb调用info proc mappings,我们获得了内存地址列表:

Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x404000     0x4000        0x0 /[...]/LambdaOutOfScope/a.out
            0x603000           0x604000     0x1000     0x3000 /[...]/LambdaOutOfScope/a.out
            0x604000           0x605000     0x1000     0x4000 /[...]/LambdaOutOfScope/a.out
            0x605000           0x637000    0x32000        0x0 [heap]

[...]

      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

但这并不能回答为什么,只有100可以更改。 问题的答案在于堆栈帧: 每个函数调用都有一个用于静态变量的局部(通常为 small )空间(为简单起见,这些空间不带new)。

如果我们使用info frame检查堆栈信息,则会看到:

(gdb) info frame
Stack level 0, frame at 0x7fffffffdbb0:
 rip = 0x400deb in main (main.cpp:75); saved rip = 0x7ffff7495830
 source language c++.
 Arglist at 0x7fffffffdba0, args: 
 Locals at **0x7fffffffdba0**, Previous frame's sp is 0x7fffffffdbb0
 Saved registers:
  rbx at 0x7fffffffdb98, rbp at 0x7fffffffdba0, rip at 0x7fffffffdba8

在main内部,所以100个不在该框架之内,因为它不是构建在main 中,而是构建在Add中,以便在添加时检查我们得到:

(gdb) info frame 1
Stack frame at 0x1:
 rip = 0x0; saved rip = <not saved>
 Outermost frame: previous frame identical to this frame (corrupt stack?)
 Arglist at 0x7fffffffdac8, args: 
 Locals at **0x7fffffffdac8**, Previous frame's sp is 0x7fffffffdad8
 Saved registers:
  rip at 0x7fffffffdad0

因此,当我们调用另一个函数时会发生损坏,但是由于main中分配的元素是本地元素,因此会保留,如果您将c.Add(400);放在300之后,则会看到它也会损坏(即使在之后构造)。

注意:我希望能涵盖所有内容,但是要详细介绍gdb的用法,互联网上有很多指南。