为什么复制捕获 lambda 在 c++20 中不是默认的 DefaultConstructible

Why are copy-capturing lambdas not default DefaultConstructible in c++20

C++20 引入了 DefaultConstructible lambda。但是,cppreference.com states 这仅适用于无状态的 lambda:

If no captures are specified, the closure type has a defaulted default constructor. Otherwise, it has no default constructor (this includes the case when there is a capture-default, even if it does not actually capture anything).

为什么这不扩展到捕获 DefaultConstructible 事物的 lambda?例如,为什么 [p{std::make_unique<int>(0)}](){ return p.get(); } 不能是 DefaultConstructible,而捕获的 p 会是 nullptr

编辑:对于那些问我们为什么想要这个的人来说,这种行为看起来很自然,因为在调用要求函子可默认构造的标准算法时,人们被迫写这样的东西:

struct S{
  S() = default;
  int* operator()() const { return p.get(); }
  std::unique_ptr<int> p;
};

所以,我们可以传入 S{std::make_unique<int>(0)},它做同样的事情。

似乎能够编写 [p{std::make_unique<int>(0)}](){ return p.get(); } 比创建一个做同样事情的结构要好得多。

不这样做的原因有两个:概念和安全。

尽管某些 C++ 程序员希望如此,但 lambda 并不意味着是具有重载 operator() 的结构的短语法。这就是 C++ lambda 的组成部分,但这不是 lambda .

从概念上讲,C++ lambda 应该是 lambda 函数 的 C++ 近似值。捕获功能并不意味着是一种编写结构成员的方法;它应该模仿 lambdas 的适当词法范围功能。这就是它们存在的原因。

在其定义的词法范围之外创建这样的 lambda(最初,不是通过 copy/move 现有的 lambda)在概念上是空洞的。编写绑定到词法范围的东西,然后在其构建范围之外创建它是没有意义的。

这也是您无法访问 lambda 之外的那些成员的原因。因为,即使它们可能是 public 成员,它们的存在也是为了实现适当的词法范围。它们是实施细节。

构造一个“捕获变量”而不实际捕获任何东西的“lambda”仅从 meta-programming 的角度来看才有意义。也就是说,只有在关注 lambda 恰好由什么组成而不是它们是什么时才有意义。 lambda 实现为 C++ 结构,捕获作为成员,并且捕获表达式在技术上什至不必命名局部变量,因此理论上可以对这些成员进行值初始化。

如果您对概念论证不服气,我们来谈谈安全性。你想要做的是声明任何 lambda 应该是默认可构造的,如果它的所有捕获都是 non-reference 捕获并且是默认可构造类型。这会招致灾难。为什么?

因为很多这样的 lambda 的作者没有要求。如果 lambda 通过从指向对象的变量移动来捕获 unique_ptr<T>,则该 lambda 内部的代码假设捕获的值指向对象是 100% 有效的(根据当前规则)。默认构造虽然在语法上有效,但在这种情况下在语义上是无意义的。

有了适当的命名类型,用户可以轻松控制它是否默认可构造。因此,如果默认构造特定类型没有意义,他们可以禁止它。对于 lambda,没有这样的语法;你必须把答案强加给每个人。 最安全 捕获 lambda 的答案,即保证永远不会破坏代码的答案,是“不”。

相比之下,无捕获 lambda 的默认构造永远不会不正确。这样的函数是“纯”的(相对于函子的内容,因为函子没有内容)。这也符合上述概念论点:无捕获 lambda 没有适当的词法范围,因此在任何地方产生它,即使在其原始范围之外,也可以。

如果您想要命名结构的行为...只需创建一个命名结构即可。您甚至不需要 default 默认构造函数;默认情况下你会得到一个(如果你没有声明其他构造函数)。