【问题标题】:Why do fields in non-mutable lambdas use "const" when capturing const values or const references?为什么非可变 lambda 中的字段在捕获 const 值或 const 引用时使用“const”?
【发布时间】:2021-10-14 11:38:05
【问题描述】:

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

但是为什么对非可变 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

v 值在第一个示例set_f1 中被复制不少于三次。

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

但是,如果字段是const,则 lambda 仿函数对象的移动构造函数不能对字段使用移动构造函数(因为这样的构造函数在窃取其内容后无法“清除” const 变量) .这就是为什么必须对这些字段使用复制构造函数。

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

【问题讨论】:

标签: c++ lambda constants language-lawyer mutable


【解决方案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 的示例不会产生const 限定的数据成员v。我们可能还会注意到我们认可该措辞

在引用的情况下,对象类型是引用所指的类型

在 lambdas 最终措辞的[expr.prim.lambda.capture]/10 中存在(但直接说明 数据成员的类型,而不是对象类型): p>

这种数据成员的类型如果实体是对对象的引用,则为被引用类型,如果实体是对函数的引用,则为对被引用函数类型的左值引用,或者否则为对应捕获实体的类型。

发生了什么

重写了 N2550 的大部分措辞:

在 2009 年 3 月的 Summit 会议期间,大量与 C++0x 相关的问题 Lambdas 由核心工作组 (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 表达式,这可能是一个微妙的错误来源。

之后,措辞(从 N2927 开始)变成了我们看到的最终进入 C++11 的措辞

此类数据成员的类型 如果实体不是对 对象,否则引用类型。

如果我敢推测,CWG 756 的决议还意味着为引用类型的实体的值捕获保留 cv 限定符,这可能是一个疏忽。

【讨论】:

  • 我不确定这是否真的回答了这个问题。这似乎是是否将mutable设为默认值。 mutable 当前所做的是删除operator()(...) 上的const 关键字,而不是捕获的值类型。请注意,即使在可变 lambda 中,您也无法修改按值捕获的参数 const T&amp;。我更新了我的问题以包含编译器生成的 lambda 类。
  • @drfib 在我的阅读中,该论文松散地使用了“隐式捕获 const”的语言,无论是由于误解还是过于简单化。它给出的示例中的 C++11 闭包类型没有 const 成员,只是一个 const 调用运算符。考虑到在 OP 中不起作用的 mutable 关键字,可以避免这种 const-ness。换句话说,我同意它并没有像我解释的那样完全回答问题。
  • @JeffGarrett 我刚刚更新(/重写)了更多历史点的答案。看起来像最终的措辞,w.r.t。它对为对象引用的实体保留 cv 限定符的影响(当值捕获时)是一个疏忽。
  • 好挖掘。 CWG 756 为我解答。他们希望 lambda 内部标识符的使用与 lambda 外部标识符(模板推导、重载解析等)相同,即使它是一个副本。如果这是基本原理,那么将 ref-to-const 作为 const 成员的按值捕获与此一致。我不同意这个决定,但很高兴知道为什么做出这个决定。
  • 对于非可变 lambda,观察 const 与 no-const 的方法较少,但它是可观察的。如果你从源中移动构造了另一个 lambda,如果它们是非常量的,则先前的 lambda 将与成员的移动外壳一起留下。然后,呼叫操作员会观察到这些成员发生了变化,这是在 lambda 之外无法观察到的。所以,这对我来说似乎也是一致的。
猜你喜欢
  • 1970-01-01
  • 2018-10-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-07
  • 1970-01-01
  • 2020-06-10
  • 1970-01-01
相关资源
最近更新 更多