为什么 [=]{} 会有 lambda 捕获?

24

直观上来看,一个不需要携带状态(通过引用或其他方式)的 lambda 函数应该可以干净地转换为裸函数指针。然而,我最近惊讶地看到以下代码在 GCC、Clang 和 MSVC 上失败:

int main(int, char *[]) {
    void (*fp)() = []{}; // OK
  //fp = [=]{};          // XXX - no user defined conversion operator available
  //fp = [&]{};          // XXX - same ...
}

C++17规范(或至少可见公共草案版本N4713)在§ 8.4.5.1的第7项中提到了有捕获和无捕获的lambda表达式:对于没有lambda-capture约束条件被满足的非泛型lambda-expression,其闭包类型具有转换函数,可以转换为具有与闭包类型的函数调用运算符相同的参数和返回类型的C ++语言链接函数指针。然而,在正式语法中,您可以在§ 8.4.5 [expr.prim.lambda] 看到以下内容:lambda-expression: lambda-introducer compound-statement ... lambda-introducer:[lambda-captureopt] ...以及在§ 8.4.5.2 [expr.prim.lambda.capture] 中:
  • lambda-capture :
    • capture-default : 捕获默认值
    • capture-list : 捕获列表
    • capture-default, capture-list : 捕获默认值和捕获列表
  • capture-default :
    • & : 引用捕获
    • = : 值捕获

所以所有编译器都遵守了法律条文,令我失望...

为什么这种存在于声明中的狭隘语法区别而不是基于函数体是否包含对任何非静态/捕获状态的引用来定义捕获呢?


1
你假设 [=][&] 没有捕获 fp,但它们确实捕获了。 - David G
7
为什么他们会捕获fp,当它在函数体中没有被使用? - Igor Tandetnik
5
我认为当前标准文本中所规定的方式只是最容易书写的方式。如果你明显不想捕获任何内容,我可以反过来问你为什么要使用 '[=]'。 - Brian Bi
2
@Brian,这显然是一个编造的最小示例。我的真正用例涉及可变参数完美转发函数,它返回一个lambda,在某个地方被封装在类型抹除类中以便检查其状态是否为有状态。解决方法是内部测试sizeof...(args),并产生两个几乎相同的lambda主体之一,但这是一个丑陋的模式。 - Jeff
2个回答

12
允许将lambda表达式转换为函数指针的变化是由一个国家机构的评论所发起的。请参见 n3052: Converting Lambdas to Function Pointers,其中提到了国家机构的 评论 UK 42

一个没有捕获列表的lambda在语义上与常规函数类型相同。通过要求这种映射,我们可以得到具有已知API并且与现有操作系统和C库函数兼容的高效lambda类型。

N3052 的决议如下:

决议:添加新段落:“具有空捕获集合的lambda表达式应可转换为函数指针类型 R(P),其中R表示返回类型,P表示lambda表达式的参数类型列表。” 此外,还可以(a)允许转换为函数引用和(b)允许 extern "C" 函数指针类型。

...

在第5段后添加一个新段落。此次编辑的意图是为了获得一个没有lambda捕获的闭包-to-函数指针转换。

一个没有lambda-capture的lambda表达式的闭包类型具有一个公共的非虚拟、非显式const转换函数,将其转换为与闭包类型的函数调用运算符具有相同参数和返回类型的函数指针。此转换函数返回的值应是一个函数的地址,当调用该地址时,具有与闭包类型的函数调用运算符相同的效果。

这就是我们今天所处的位置。请注意,注释中说到“空捕获列表”,而我们今天所拥有的似乎符合注释中所述的意图。

看起来这是一个基于国家机构评论的修复,并且被狭窄地应用。


3
您提出的规则非常脆弱,特别是在 P0588R1 之前的世界中,隐式捕获依赖于 odr-use。
请考虑以下情况:
void g(int);
void h(const int&);

int my_min(int, int);

void f(int i) {
    const int x = 1, y = i;
    [=]{ g(x); }; // no capture, can convert?
    [=]{ g(y); }; // captures y
    [=]{ h(x); }; // captures x
    [=]{ my_min(x, 0); }; // no capture, can convert?
    [=]{ std::min(x, 0); }; // captures x
}

1
但显然在那个时候,脆弱性不是一个问题,因为默认参数中lambda的规范使用了脆弱规则:void f() { const int x = 1; void a1(int = [=]{ return x; }()); /* <- OK, no capture */ void a2(int = [=]{ return *&x; }()); /* <- Error */ } :/ - cpplearner
如果在整个C++的历史中,所有程序员都没有暴露出那种脆弱性,而委员会中似乎也没有人关心,那么我可以假装关心它太脆弱了。 - curiousguy
1
本地函数声明很少见;带有默认参数的本地函数声明更为罕见;使用lambda表达式的带有默认参数的本地函数声明更是凤毛麟角。 - T.C.
1
@cpplearner 当程序员被期望正确使用更简单、面向初学者的特性,如内联函数 const int x = 1; inline int f() { return x; } /* <- OK */ inline int g() { return *&x; } /* <- ill formed no diag required */(如果包含在多个TU中),脆弱性从来不是一个问题。 - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接