为什么不可变 lambda 中的字段在捕获 const 值或 const 引用时使用“const”?

Why do fields in non-mutable lambdas use "const" when capturing const values or const references?

提问人:Emil 提问时间:8/11/2021 最后编辑:Emil 更新时间:8/11/2021 访问量:263

问:

如问题 lambda capture by value mutable does not work with const &?,当使用其名称或在可变 lambda 中捕获类型的值时,隐藏类中的字段会获取 type 。可以说,对于可变 lambda 来说,这是正确的做法。const T&[=]const T

但是,为什么对不可变的 lambda 也要这样做呢?在不可变的 lambda 中,声明了 ,因此它无论如何都无法修改捕获的值。operator()(...)const

当我们移动 lambda 时,就会发生这种情况的不良后果,例如,将其包装在 .std::function

请参阅以下两个示例:

#include <cstdio>
#include <functional>

std::function<void()> f1, f2;

struct Test {
    Test() {puts("Construct");}
    Test(const Test& o) {puts("Copy");}
    Test(Test&& o) {puts("Move");}
    ~Test() {puts("Destruct");}
};

void set_f1(const Test& v) {
    f1 = [v] () {}; // field type in lambda object will be "const Test"
}

void set_f2(const Test& v) {
    f2 = [v = v] () {}; // field type in lambda object will be "Test"
}

int main() {
    Test t;
    puts("set_f1:");
    set_f1(t);
    puts("set_f2:");
    set_f2(t);
    puts("done");
}

我们得到以下编译器生成的 lambda 类:

class set_f1_lambda {
    const Test v;
public:
    void operator()() const {}
};

class set_f2_lambda {
    Test v;
public:
    void operator()() const {}
};

程序打印以下内容(使用 gcc 或 clang):

Construct
set_f1:
Copy
Copy
Copy
Destruct
Destruct
set_f2:
Copy
Move
Move
Destruct
Destruct
done
Destruct
Destruct
Destruct

在第一个示例中,该值被复制不少于三次。vset_f1

在第二个示例中,唯一的副本是在捕获值时(如预期的那样)。使用两个移动的事实是 libstdc++ 中的一个实现细节。当按值将函子传递给内部函数时,第一个移动发生在内部(为什么这个函数签名不使用引用传递?第二个移动发生在移动构造最终堆分配的函子时。set_f2operator=std::function

但是,如果字段是字段,则 lambda 函子对象的 move 构造函数不能对字段使用 moveconstructor(因为此类构造函数在窃取其内容后无法“清除”常量变量)。这就是为什么必须对此类字段使用复制构造函数的原因。const

所以对我来说,它似乎只对捕获不可变的 lambda 中的值产生负面影响。我是否错过了一些重要的东西,或者它只是以这种方式标准化以使标准更简单?const

C++(英语:C++) λ 常数 语言律师 可变

评论

0赞 Jeff Garrett 8/11/2021
类似的问题没有令人满意的答案:stackoverflow.com/questions/56811624/......

答:

4赞 dfrib 8/11/2021 #1

我是否错过了一些重要的东西,或者它只是以这种方式标准化以使标准更简单?

原始的 lambda 提案,

区分捕获对象的类型和 lambda 闭包类型的相应数据成员的类型:

/6 闭包对象的类型是具有唯一名称的类,称为 F,认为是在 发生 lambda 表达式。

在上下文中查找有效捕获集中的每个名称 N 其中 lambda 表达式似乎用于确定其对象类型;对于引用,对象类型是引用所引用的类型。对于有效捕获集中的每个元素,F 具有私有非静态数据成员,如下所示:

  • 如果元素是 this,则数据成员具有某个唯一名称,将其命名为 t,并且属于 this ([class.this], 9.3.2);
  • 如果元素的形式为 & N,则数据成员的名称为 N,并键入“对对象类型的 N 的引用”; 5.19. 常量表达式 3
  • 否则,元素的形式为 N,数据成员的名称为 N,键入“cv-unqualified object type of N”。

在这个原始措辞中,OP 的示例不会产生 -qualified data 成员。我们还可以注意到,我们承认这些措辞constv

对于引用,对象类型是引用所引用的类型

它存在于 [expr.prim.lambda.capture]/10 的 [expr.prim.lambda.capture]/10 中(最新的草案)中关于 lambda 的最终措辞:

如果实体是对对象的引用,则此类数据成员的类型为引用类型,如果实体是对函数的引用,则为对引用函数类型的左值引用,否则为相应捕获实体的类型。

发生的事情是

它重写了 N2550 的大部分措辞:

在 2009 年 3 月的峰会期间,大量与 C++x 相关的问题 Lambda 由核心工作组 (CWG) 提出并审核。在决定清除后 对于大多数问题,CWG得出结论认为,最好重写该部分 在 Lambdas 上实现该方向。本文介绍了这种重写。

特别是,就本问题而言,解决CWG问题

[...]请看以下示例:

void f() {
  int const N = 10;
  [=]() mutable { N = 30; }  // Okay: this->N has type int, not int const.
  N = 20;  // Error.
}

也就是说,作为闭包对象成员的 N 不是 const, 即使捕获的变量是 const。 这似乎很奇怪,因为 捕获基本上是捕获本地环境的一种手段。 避免终生问题的方式。更严重的是,类型的变化 表示 decltype、重载解析和模板的结果 应用于 lambda 中捕获的变量的参数推导 表达式可以与包含 lambda 表达式,这可能是 bug 的微妙来源。

之后,措辞(截至 N2927)被制成我们最终看到的 C++ 11

此类数据成员的类型 是相应捕获实体的类型,如果该实体不是对 对象,否则为引用类型。

如果我敢推测,CWG 756的决议还意味着保留简历限定符,用于参考类型的实体的价值捕获,这可以说是一个疏忽。

评论

0赞 Emil 8/11/2021
我不确定这是否真的回答了这个问题。这似乎是是否默认。目前所做的是删除关键字 on ,而不是捕获的值类型。请注意,即使在可变 lambda 中,您也无法修改 captured-by-value 参数。我更新了我的问题以包括编译器生成的 lambda 类。mutablemutableconstoperator()(...)const T&
1赞 Jeff Garrett 8/11/2021
@drfib 据我所知,那篇论文松散地使用了“隐式捕获”的语言,要么是由于误解,要么是过于简单化。它给出的示例中的 C++11 闭包类型没有 const 成员,只有一个 const 调用运算符。考虑到可变关键字,可以避免这种常量性,这在 OP 中不起作用。换句话说,我同意它并没有完全回答我所解释的问题。
0赞 dfrib 8/11/2021
@JeffGarrett 我刚刚更新(/重写)了一些更多历史点的答案。似乎最终的措辞,w.r.t.它对保留引用对象的实体的 cv 限定符的影响(当价值捕获时),是一个疏忽。
0赞 Jeff Garrett 8/11/2021
好挖。CWG 756为我解答了这个问题。他们希望 lambda 内部的标识符与 lambda 外部的标识符(模板推导、重载解析等)的使用相同,即使它是副本。如果这是基本原理,那么将 ref-to-const 作为 const 成员的按值捕获与此一致。我不同意这个决定,但很高兴知道为什么会做出这个决定。
3赞 Jeff Garrett 8/11/2021
对于不可变的 lambda,观察常量与无常量的方法较少,但它是可观察的。如果您从源代码移动构造另一个 lambda,则如果成员的 lambda 是非常量的,则先一个 lambda 将保留从成员的 shell。然后,调用操作员将观察到这些成员已更改,这是在 lambda 之外无法观察到的。所以,在我看来,这里也是一致的。