0

我不明白为什么捕获的值会丢失。我了解它与 LambdaWrapper 对象的超出范围或复制有关。但究竟会发生什么?如果 LambdaWrapper(100) 离开 Add 中的范围并且对 __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
4

3 回答 3

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 甚至有一个错误\缺陷,其中 lambda 的成员可以从外部访问,因为它们不是私有的。

于 2019-05-26T12:02:15.820 回答
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;
};
于 2019-05-26T12:07:44.527 回答
1

我想这个问题是在问会发生什么,而不是“可以吗”,所以在这种情况下(通常)gdb是你的朋友,修改程序以在构造过程中打印 this 的地址,this 在 __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堆上分配数据。

通过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。问题的答案在于堆栈框架:每个函数调用都有一个本地(通常很小)变量空间用于静态变量(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, 中构造的,当在 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 中分配的元素是 local 它们被保留,如果您c.Add(400);在 300 之后放置 a ,您也会看到它被损坏(即使在之后构造)。

注意:我希望已经涵盖了所有内容,但 gdb 的使用细节,互联网上有很多指南。

于 2019-05-26T13:18:40.340 回答