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

Emi*_*mil 7 c++ lambda constants mutable language-lawyer

正如问题lambda capture by value mutable 中所见,它不适用于 const &? , 当const T&使用其名称或[=]可变 lambda捕获类型的值时,隐藏类中的字段获取类型const T。可以争论的是,这对于可变 lambda 来说是正确的做法。

但是为什么对非可变 lambda 也这样做呢?在非可变 lambda 表达式中,operator()(...)is 被声明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");
}
Run Code Online (Sandbox Code Playgroud)

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

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

class set_f2_lambda {
    Test v;
public:
    void operator()() const {}
};
Run Code Online (Sandbox Code Playgroud)

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

Construct
set_f1:
Copy
Copy
Copy
Destruct
Destruct
set_f2:
Copy
Move
Move
Destruct
Destruct
done
Destruct
Destruct
Destruct
Run Code Online (Sandbox Code Playgroud)

v在第一个示例中,该值被复制不少于 3 次set_f1

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

但是,如果字段是 const(因为这样的构造函数在窃取其内容后无法“清除”const 变量)。这就是为什么必须将复制构造函数用于此类字段的原因。

所以对我来说,像const在非可变 lambdas 中一样捕获值似乎只有负面影响。我是否遗漏了一些重要的东西,或者它只是通过这种方式标准化以某种方式使标准更简单?

dfr*_*fri 4

\n

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

\n
\n

最初的 lambda 提案,

\n\n

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

\n
\n

/6 闭包对象的类型是一个具有唯一名称的类,称之为 F,被认为是在 lambda 表达式出现的位置定义的。

\n

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

\n
    \n
  • 如果元素是 this,则数据成员有一些唯一的名称,称之为 t,并且是 this 的类型 ([class.this],\n9.3.2);
  • \n
  • 如果元素的形式为 & N,则数据成员的名称为 N,类型为 \xe2\x80\x9c,对 N\xe2\x80\x9d 的对象类型的引用;\n5.19。常数表达式 3
  • \n
  • 否则,元素的形式为 N,数据成员的名称为 N,类型为 \xe2\x80\x9c N\xe2\x80\x9d 的cv 非限定对象类型
  • \n
\n
\n

在这个原始措辞中,OP 的示例不会产生const- 限定的数据成员v。我们还可能注意到,我们认识到这样的措辞

\n
\n

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

\n
\n

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

\n
\n

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

\n
\n

发生的事情是

\n\n

重写了N2550的大部分措辞:

\n
\n

在 2009 年 3 月的 Summit 会议上,核心工作组 (CWG) 提出并审查了与 C++0x\nLambda 相关的大量问题。在为大多数问题确定了明确的方向后,CWG 得出的结论是,最好重写 Lambda 部分来实现该方向。本文介绍了这种重写。

\n
\n

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

\n\n
\n

[...]考虑以下示例:

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

也就是说,作为闭包对象成员的 N 不是 const,\n即使捕获的变量是 const。这看起来很奇怪,因为捕获基本上是一种捕获本地环境的方法,可以避免生命周期问题。更严重的是,类型的更改意味着应用于 lambda 表达式内捕获变量的 decltype、重载解析和模板参数推导的结果可能与包含 lambda 表达式的作用域中的结果不同,这可能是错误的微妙来源。

\n
\n

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

\n
\n

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

\n
\n

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

\n

  • 对于非可变 lambda,观察 const 与 no-const 的方法较少,但它是可观察的。如果您从源中移动构造了另一个 lambda,则先前的 lambda 将保留成员的移出 shell(如果它们是非常量)。然后,调用运算符会观察到这些成员已发生更改,而这在 lambda 之外是无法观察到的。所以,这对我来说似乎也是一致的。 (3认同)