在嵌套 lambda 的情况下如何初始化 lambda 捕获?

Posted

技术标签:

【中文标题】在嵌套 lambda 的情况下如何初始化 lambda 捕获?【英文标题】:How are lambda captures initialized in case of nested lambdas? 【发布时间】:2020-09-14 17:37:31 【问题描述】:

好的,这就是 n3337.pdf 中的 [expr.prim.lambda]p16 的直接内容。下面以代码为例:

int a = 1, b = 1, c = 1;
auto m1 = [a, &b, &c]() mutable

    auto m2 = [a, b, &c]() mutable
    
        std::cout << a << b << c;     // Shouldn't this print 113 or 133?
        a = 4; b = 4; c = 4;
    ;
    a = 3; b = 3; c = 3;
    m2();
;
a = 2; b = 2; c = 2;
m1();
std::cout << a << b << c;             // Okay, this prints 234

并且它应该生成以下输出:

123234

但是,我理解 [expr.prim.lambda] 中文本的方式(这在某种程度上明显有缺陷),我觉得输出应该是113234,特别是b 的值打印在m2 .以下是我的理解/解释:

std::cout &lt;&lt; a &lt;&lt; b &lt;&lt; c;m2 内部执行时,根据 [expr.prim.lambda]p16(强调我的):

如果 lambda 表达式 m2 捕获了一个实体,并且该实体被直接封闭的 lambda 表达式捕获 m1,那么 m2 的捕获变换如下:

——如果m1通过副本捕获实体,则m2捕获m1闭包类型对应的非静态数据成员;

因此,m2 内的a 应将生成的成员捕获到闭包类型m1 中捕获的对应a。由于m1 中的a 按副本捕获,而m2 中的a 也按副本捕获,因此am2 中的值应为1

标准继续说(再次强调我的):

——如果 m1 通过引用捕获实体,则 m2 捕获由 m1 捕获的相同实体

我相信这里的“同一实体”是指m1 通过引用捕获的实体,而当m2 捕获时,它应该是 - 如果是捕获,则引用同一实体通过引用,或者如果它是一个副本捕获它的副本。

因此,m2 中的 b 应指在两个 lambda 表达式之外定义的 b。那么m2b 的值应该是1,因为b 也是通过副本捕获的。

我哪里出错了?更具体地说,b 内的 m2 何时初始化?

【问题讨论】:

【参考方案1】:

首先,请注意,根据 C++11 [expr. prim.lambda] 第 14 段(或 C++17 [expr.prim.lambda.capture] paragraph 10)。

您从 C++11 [expr.prim.lambda]/16(或 C++17 [expr.prim.lambda.capture]/13 中的相同内容)引用的部分仅更改捕获的实体,而不更改捕获的类型。因此,在示例中,用于初始化 m2 的内部 lambda 通过复制从原始定义中捕获 b

然后,注意 C++11 [expr.prim.lambda]/21:

lambda-expression 被求值时,被拷贝捕获的实体被用来直接初始化结果闭包对象的每个对应的非静态数据成员。

(C++17 [expr.prim.lambda.capture]/15 开头相同,但为 init-capture 语法添加了额外的措辞,例如 [var=init]。)

在示例中,初始化 m2 的内部 lambda-expression 被求值,b 的闭包对象的成员被初始化,每次调用 m1.operator(),而不是在命令 lambda-expression 出现在代码中。由于 m2 的 lambda 通过复制捕获原始 b,因此它在调用 m1 时获取 b 的值。如果m1 被多次调用,b 的初始值可能每次都不同。

【讨论】:

[...],初始化 m2 的内部 lambda 表达式被求值,b 的闭包对象的成员被初始化,每次调用 m1.operator(),不是按照 lambda 表达式出现在代码中的顺序。 - 你能引用标准中关于这个的参考吗?此外,我无法在标准中找到它在评估 lambda 表达式时的说明。 没有关于何时计算 lambda 表达式的规则,只有关于何时计算一般表达式的规则。这里的规则只是简单的众所周知的规则,例如“函数体或其他复合块中的语句按顺序评估”和“评估作为变量定义的语句会评估变量的任何初始化程序”。因此,内部 lambda 表达式在调用外部 lambda 表达式主体期间进行评估,原因与下面的 a = 3; ... m2(); 语句在此之后立即进行评估的原因相同。 我赞成你的回答。我认为您在回答的最后一段中所说的话现在是有道理的。所以基本上当编译器看到auto m1 = [a, &amp;b, &amp;c]() mutable ...;时,它会创建一个prvalue临时闭包对象并用它初始化m2;这还涉及在闭包类型中创建(仅用于复制捕获)未命名的数据成员并初始化它们。由于m2 的复合语句被放置在函数调用运算符中,它的执行就像任何其他函数一样,因此,m1 的闭包对象仅在调用函数调用运算符时创建,对吧? 听起来很准确,但似乎有些 m1m2 名称混淆了。 我想我困了。可惜无法修改 cmets。【参考方案2】:

——如果m1 通过引用捕获实体,m2 捕获由m1 捕获的同一实体。

是的,所以m2 的捕获列表中的b 捕获的不是引用本身(即m1 的捕获),而是它指向的对象。

但是m2 是通过值还是通过引用捕获b 完全取决于m2 的捕获列表中写入的内容。在b 之前没有&amp;,所以b 是按值捕获的。

b 内的m2 何时初始化?

当控制到达auto m2 = ...;时。此时,将检查存储在m1 中的对b 的引用,并将其指向的对象复制到m2


这里有一个更简单的解释。

当您按值捕获引用时,您会复制它所指向的对象。

当您按引用捕获引用时,您会引用它指向的对象。

这里,“捕获引用”同样适用于捕获实际引用,以及捕获封闭 lambda 的引用捕获。

【讨论】:

关于[...],检查存储在m1中的b的引用,并将它指向的对象复制到m2中,你意味着这是在运行时/程序执行期间检查的,而不是编译器解析代码?另外,当时2 中的b 中的值是如何检查的?就在调用m2(); 之前,ba = 3; b = 3; c = 3; 值更改为3。什么给了? @Cheshar 我认为是的,通常它发生在运行时。在某些情况下,它可能会根据 as-if 规则在编译时决定。 “另外,那个时候2的b中的值是怎么检查的?” 可能我不够清楚;我的意思是“当控制到达声明 auto m2 = ... 本身时检查”,而不是“当控制进入 m2 时”。 另外,2 时 b 中的值是如何检查的? - 我想我没说对。我的意思和困惑是,m2 中的b(由副本捕获)如何捕获在程序中稍后分配给两个 lambda 之外的 b 的值。但我认为@aschepler 在他的 cmets 中提到的内容很清楚。此外,关于检查存储在 m1 中的 b 的引用,您可能需要更正此问题,因为对于引用捕获,未指定是否将在闭包类型中声明任何数据成员。 @Cheshar 很高兴你知道了! “关于存储在 m1 中的 b 的引用被检查,你可能想要更正这个” 嗯,你是对的。但我不知道如何改写这部分而不使其不太清楚。

以上是关于在嵌套 lambda 的情况下如何初始化 lambda 捕获?的主要内容,如果未能解决你的问题,请参考以下文章

主网001提案|销毁Lambda研发团队持有的7亿LAMB投票通过

为啥 Enumerable#detect 需要 Proc/lambda?

Vis.js 时间轴 - 如何在不嵌套的情况下折叠组

确定我们是否在AWS Lambda + Zappa下运行?

Lambda表达式树解析(下)

Lambda 验证节点正式接入主网公告