【问题标题】:Lambda loses captured valueLambda 失去捕获的价值
【发布时间】:2019-05-26 11:42:56
【问题描述】:

我不明白为什么捕获的值会丢失。我了解它与 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

【问题讨论】:

  • 考虑当LambdaWrapper 被复制时会发生什么。由于没有用户定义的复制构造函数,__func 成员是被复制的 __func 的精确副本 - 并且它从一个对象中捕获了 this 指针,当您调用 c.call() 时,该对象不再存在.因此行为是未定义的。另外:改掉使用带有双下划线前缀的标识符的习惯,例如 __func - 这样的标识符是为实现保留的 - 自己创建它们也会导致未定义的行为。
  • 为什么使用 300?不小心,因为内存还没有重写?感谢您提供有关双下划线前缀的提示 - 我认为私人成员可以接受它。
  • @EzR1d3r 单下划线在本地被接受。在任何地方都不允许大写和双下划线(我可能会忘记一些事情),包括预处理器宏(快速 gcc 示例 - __reserved)

标签: c++ lambda


【解决方案1】:

如果 lambda 已经是这样的包装器,为什么需要这样做?保存捕获?你做的正好相反。

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

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

此构造函数有效,因为它创建了新的 lambda,例如具有新的 this 值,该值将持续存在。 Lambda 只不过是带有指针字段(可能是__this)的类实例的语法糖,它存储this 的值并包含void operator() () {cout &lt;&lt; __this-&gt;__value &lt;&lt; endl;};

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

这种设计不是最理想的,因为每次创建一个新对象。如果只捕获值,则不需要复制构造函数。

PS。作为对 lambda 闭包性质的说明,GCC 甚至有一个错误\缺陷,其中 lambda 的成员可以从外部访问,因为它们不是私有的。

【讨论】:

    【解决方案2】:

    我想这个问题是问会发生什么,而不是“可以吗”,所以在这种情况下(通常)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)
    

    存在巨大的价值差异,这是因为创建发生在stack上,而vector在heap上分配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 留在 out 这一帧中,因为它不是在 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 中分配的元素是本地的,所以它们会被保留,如果你在 300 之后放置 c.Add(400);,你也会看到它被损坏了(即使是在之后构建的)。

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

    【讨论】:

    • LambdaWrapper(300) 在调用时不会被任何局部变量覆盖,但如果你强制另一个变量 完全不相交 wrt 它可以被覆盖:{ c.Add ( LambdaWrapper(300) ); } { 字符串 x = "什么?"; cout
    • 这是对优化返回的解释,如果映射回源代码则不正确。这是从硬件角度发生的事情,而不是从语言的抽象角度发生的事情。没有损坏或 IB,只是指向时间对象的悬空指针。
    • @Swift-FridayPie 没错,这只是当前的 gcc 行为,从语言的角度来看,它确实是未定义的行为
    【解决方案3】:

    一定要注意@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;
    };
    

    【讨论】:

    • 谢谢!我知道它与复制构造函数一起工作,我想了解会发生什么。
    • @EzR1d3r 看到我的解释,这在闭包内部发生了混乱的变化,它将作为由 lamba 创建的仿函数对象的一部分传递。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-21
    • 2013-07-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-22
    • 2019-08-03
    相关资源
    最近更新 更多