为什么有时不需要在lambda中捕获const变量?


76

考虑以下示例:

#include <cstdlib>

int main() {
    const int m = 42;
    [] { m; }(); // OK

    const int n = std::rand();
    [] { n; }(); // error: 'n' is not captured
}

为什么我需要n在第二个Lambda中捕获而不是m在第一个Lambda中捕获?我检查了C ++ 14标准中的5.1.2节(Lambda表达式),但找不到原因。您能指出我对此段的解释吗?

更新:我在GCC 6.3.1和7(主干)中都观察到了此行为。在这两种情况下(variable 'm' cannot be implicitly captured in a lambda with no capture-default specified),Clang 4.0和5(主干)均失败,并出现错误。


2
constexprvsconst
CinCout

1
如果您更改m;m + 0;
MM,

4
同样的原因,std::array<int,m>是确定,std::array<int,n>是不是。
n。代词

3
我希望constexpr除非您明确指定变量,否则不要这样。如果有人想要constexpr变量,则应将其声明为1。
xinaiz

2
@BlackMoses:m不是constexpr,它是一个编译时常量整数表达式……这是constexpr在梦keyword以求的关键字之前的事情。
Ben Voigt

Answers:


55

对于位于块范围的lambda,即使未捕获到满足范围内某些条件的变量,也可以在lambda内部以有限的方式使用它们。

粗略地讲,达到范围包括包含lambda的函数局部的任何变量,该变量将在定义lambda的范围内。因此,这包括mn在以上示例。

具体来说,“某些标准”和“有限的方式”(从C ++ 14开始):

  • 在lambda内部,不得对该变量进行odr-used,这意味着除以下内容外,不得对该变量进行任何操作:
    • 出现为舍弃值表达式(m;是其中之一),或者
    • 获取其值。
  • 该变量必须是:
    • 一个A const,非volatile整数或枚举,其初始值设定项是一个常量表达式,或者
    • A constexpr,非volatile变量(或此类的子对象)

对C ++ 14的引用:[expr.const] /2.7,[basic.def.odr] / 3(第一句话),[expr.prim.lambda] / 12,[expr.prim.lambda] / 10。

正如其他评论/答案所建议的那样,这些规则的基本原理是,编译器需要能够“合成”无捕获的lambda作为独立于块的自由函数(因为此类事情可以转换为指针,功能);如果知道变量将始终具有相同的值,即使引用了变量,它也可以执行此操作,或者可以重复该过程以独立于上下文获取变量的值。但是,如果变量可能不时有所不同,或者例如需要变量的地址,则无法执行此操作。


在您的代码中,n已通过非常量表达式初始化。因此n,未经捕获就不能在lambda中使用。

m是通过常量表达式初始化的42,因此它确实满足“某些条件”。舍弃值表达式不会使用该表达式,因此m;可以在不m被捕获的情况下使用它。gcc是正确的。


我会说两个编译器之间的区别是clang考虑m;使用odr-use m,但是gcc不会。[basic.def.odr] / 3的第一句话很复杂:

一种可变x其名称显示为潜在评估表达exODR使用的ex除非施加左值到右值转换到x产率的常量表达式不调用任何非平凡函数,并且如果x是一个对象,ex是的一个元素表达式的潜在结果集e,其中将左值到右值转换应用于e,或者e是舍弃值表达式。

但是,仔细阅读后,它确实特别提到了舍弃值表达式不会使用该表达式。

C ++ 11的[basic.def.odr]版本最初不包含舍弃值表达式大小写,因此在已发布的C ++ 11下clang的行为将是正确的。但是,C ++ 14中出现的文本被视为针对C ++ 11的缺陷(问题712),因此,即使在C ++ 11模式下,编译器也应更新其行为。


我认为[expr] / 11表示在这种情况下不应用左值到右值转换。
cpplearner

@cpplearner我之前没有看到过,但是您指出来之后我同意了,谢谢...将更新我的答案
MM

再读一遍,似乎m;执行左值到右值的转换似乎无关紧要。所述的定义ODR使用[basic.def.odr] / 3说“无论是左值到右值转换应用于e,或者e是一个被丢弃值表达式”,这里的表达m是一个丢弃值表达式,因此最终该变量m未被使用。
cpplearner

@cpplearner Roger,我误读了“舍弃值表达式”为“未评估表达式”
MM

34

因为它是一个常量表达式,所以编译器将其视为 [] { 42; }();

[ expr.prim.lambda ]中的规则是:

如果一个lambda表达式或一个通用lambda odr的函数调用操作符模板的实例(3.2)使用该变量或具有自动存储持续时间的变量(从其到达范围开始),该实体应由lambda-expression捕获。

这里是标准[ basic.def.odr ]的引文:

除非使用x的左值到右值转换会产生一个常数表达式(...)或e是一个废弃值表达式,否则将使用其名称作为可能值表达式ex表示的变量x。

(删除的部分并不重要,以使其简短)

我的简单理解是:编译器知道m在编译时是常量,而n在运行时会更改,因此n必须捕获。n会被滥用,因为您必须实际查看n运行时内部的内容。换句话说,“只有一个”定义的事实n是相关的。

摘自MM的评论:

m是一个常量表达式,因为它是带有常量表达式初始值设定项的const自动变量,但n不是常量表达式,因为它的初始值设定项不是常量表达式。[expr.const] /2.7中对此进行了介绍。根据[basic.def.odr] / 3的第一句话,常量表达式未使用ODR

看到这里的演示


5
最好再详细解释为什么n; 使用odr nm;使用odr m
MM

1
我看不到该段的相关性,因为它说该实体被lambda表达式捕获。在第一个lambda中,没有捕获,但是GCC对其进行了编译。
s3rvac

@ s3rvac参见MM的评论这里的关键是在一种情况下使用odr,但在另一种情况下则没有。
伊利亚·波波夫

@IlyaPopov在两个lambda中使用变量的方式相同,因此我看不到为什么在一个lambda中而不是在另一个lambda中应该使用odr-use。
s3rvac

2
@ s3rvac,该段显示“如果[满足条件],则该实体应被捕获”。满足条件n但不满足m
MM

2

编辑:我的答案的先前版本是错误的。初学者是正确的,这里是相关的标准报价:

[basic.def.odr]

  1. 变量x的名称显示为可能评估的表达式ex ,否则ex会使用它,除非对x应用左值到右值转换会产生不调用任何非平凡函数的常量表达式,并且如果x是对象,ex是表达式e的一组潜在结果的元素,其中将左值到右值转换应用于e或e是一个废弃值表达式。...

由于m是一个常数表达式,因此它不会被过度使用,因此不需要被捕获。

c声似乎与标准不符。


第一个实际可行
初学者

我相信可以,请参阅标准中的报价。你怎么看?
初学者

@Beginner引用实际上证实了我的答案。“必须”对程序施加了限制。这意味着必须捕获变量。由于未捕获它(使用默认捕获既不是显式的也不是隐式的),因此程序违反了该规则。
eerorika

达到范围的自动变量是常量表达式,不需要捕获
MM

1
@MM很有意思。你能找到这样的规则吗?
eerorika
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.