Lambda回归:这合法吗?

Lambda returning itself: is this legal?

本文关键字:回归 Lambda      更新时间:2023-10-16

考虑这个相当无用的程序:

#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}

基本上,我们试图制造一个自我返回的lambda。

  • MSVC 编译程序,并运行
  • GCC编译程序,它段错误
  • Clang 拒绝该程序,并显示一条消息:

    error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined

哪个编译器是正确的?是否存在静态约束冲突、UB 或两者都没有?

更新这个轻微的修改被 clang 接受:

auto it = [&](auto& self, auto b) {
std::cout << (a + b) << std::endl;
return [&](auto p) { return self(self,p); };
};
it(it,4)(6)(42)(77)(999);

更新 2:我了解如何编写返回自身的函子,或者如何使用 Y 组合器来实现这一点。这更像是一个语言律师的问题。

更新 3:问题在于 lambda 一般返回自身是否合法,而在于这种特定方式的合法性。

相关问题:C++ lambda 返回自身。

该程序格式不正确(clang 是对的)根据 [dcl.spec.auto]/9:

如果表达式中出现具有未推断占位符类型的实体的名称,则程序格式不正确。但是,一旦在函数中看到未丢弃的返回语句,从该语句推导出的返回类型就可以用于函数的其余部分,包括其他返回语句。

基本上,内部 lambda 的返回类型的推导取决于自身(此处命名的实体是调用运算符) - 因此您必须显式提供返回类型。在这种特殊情况下,这是不可能的,因为您需要内部 lambda 的类型,但无法命名它。但是在其他情况下,尝试像这样强制递归 lambda 是可行的。

即使没有这个,你也有一个悬而未决的参考。


在与更聪明的人(即 T.C.) 讨论后,让我再详细说明一些。原始代码(略有减少)和建议的新版本(同样减少)之间存在重要差异:

auto f1 = [&](auto& self) {
return [&](auto) { return self(self); } /* #1 */ ; /* #2 */
};
f1(f1)(0);
auto f2 = [&](auto& self, auto) {
return [&](auto p) { return self(self,p); };
};
f2(f2, 0);

那就是内部表达式self(self)不依赖于f1,而是self(self, p)依赖于f2。当表达式是非依赖的时,可以使用它们...急切地([temp.res]/8,例如,无论它发现自己所在的模板是否实例化,static_assert(false)如何是一个硬错误)。

对于f1,编译器(比如clang)可以尝试急切地实例化它。一旦你在上面的第#2点到达该;,你就知道外部 lambda 的推导类型(它是内部 lambda 的类型),但我们试图更早使用它(把它想象成在第#1点) - 我们试图在我们仍在解析内部 lambda 时使用它, 在我们知道它实际上是什么类型之前。这与dcl.spec.auto/9相冲突。

但是,对于f2,我们不能尝试急切地实例化,因为它是依赖性的。我们只能在使用点实例化,此时我们知道一切。


为了真正做这样的事情,你需要一个y-combinator。论文的实现:

template<class Fun>
class y_combinator_result {
Fun fun_;
public:
template<class T>
explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}
template<class ...Args>
decltype(auto) operator()(Args &&...args) {
return fun_(std::ref(*this), std::forward<Args>(args)...);
}
};
template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

你想要的是:

auto it = y_combinator([&](auto self, auto b){
std::cout << (a + b) << std::endl;
return self;
});

编辑 关于这种结构是否严格C++有效,似乎存在一些争议。普遍的看法似乎是它无效。有关更彻底的讨论,请参阅其他答案。如果构造有效,则此答案的其余部分适用;下面调整后的代码适用于 MSVC++ 和 gcc,OP 发布了进一步修改的代码,这些代码也适用于 Clang。

这是未定义的行为,因为内部 lambda 通过引用捕获参数self,但在第 7 行return之后self超出范围。因此,当稍后执行返回的 lambda 时,它正在访问对超出范围的变量的引用。

#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self); // <-- using reference to 'self'
};
};
it(it)(4)(6)(42)(77)(999); // <-- 'self' is now out of scope
}

使用valgrind运行程序说明了这一点:

==5485== Memcheck, a memory error detector
==5485== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5485== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5485== Command: ./test
==5485== 
9
==5485== Use of uninitialised value of size 8
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485== 
==5485== Invalid read of size 4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  Address 0x4fefffdc4 is not stack'd, malloc'd or (recently) free'd
==5485== 
==5485== 
==5485== Process terminating with default action of signal 11 (SIGSEGV)
==5485==  Access not within mapped region at address 0x4FEFFFDC4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  If you believe this happened as a result of a stack
==5485==  overflow in your program's main thread (unlikely but
==5485==  possible), you can try to increase the size of the
==5485==  main thread stack using the --main-stacksize= flag.
==5485==  The main thread stack size used in this run was 8388608.

相反,您可以将外部 lambda 更改为通过引用而不是按值获取 self,从而避免了一堆不必要的副本并解决了问题:

#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto& self) { // <-- self is now a reference
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}

这有效:

==5492== Memcheck, a memory error detector
==5492== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5492== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5492== Command: ./test
==5492== 
9
11
47
82
1004

TL;博士;

叮当是对的。

看起来使这种格式不正确的标准部分是 [dcl.spec.auto]p9:

如果表达式中出现具有未推断占位符类型的实体的名称,则程序是 格式不正确。但是,一旦在函数中看到未丢弃的返回语句,返回类型 从该语句推导的函数可用于函数的其余部分,包括其他 return 语句。 [ 示例:

auto n = n; // error, n’s initializer refers to n
auto f();
void g() { &f; } // error, f’s return type is unknown
auto sum(int i) {
if (i == 1)
return i; // sum’s return type is int
else
return sum(i-1)+i; // OK, sum’s return type has been deduced
}

—结束示例 ]

原创作品通过

如果我们看一下提案 A 将 Y Combinator 添加到标准库的提案,它提供了一个可行的解决方案:

template<class Fun>
class y_combinator_result {
Fun fun_;
public:
template<class T>
explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}
template<class ...Args>
decltype(auto) operator()(Args &&...args) {
return fun_(std::ref(*this), std::forward<Args>(args)...);
}
};
template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

它明确表示您的示例是不可能的:

C++11/14 lambda 不鼓励递归:无法从 lambda 函数的主体引用 lambda 对象。

它引用了理查德·史密斯(Richard Smith)暗示了Clang给你的错误:

我认为这作为一流的语言功能会更好。我没有时间参加 Kona 会议前的会议,但我打算写一篇论文,允许给 lambda 一个名字(范围限定在它自己的身体):

auto x = []fib(int a) { return a > 1 ? fib(a - 1) + fib(a - 2) : a; };

在这里,'fib' 相当于 lambda 的 *this(尽管 lambda 的闭包类型不完整,但有一些烦人的特殊规则允许它工作)。

Barry 向我指出了后续提案 递归 lambdas,它解释了为什么这是不可能的,并绕过了dcl.spec.auto#9限制,并展示了今天在没有它的情况下实现这一目标的方法:

Lambda 是本地代码重构的有用工具。但是,我们有时希望从自身内部使用 lambda,以允许直接递归或允许将闭包注册为延续。在目前的C++中,这很难很好地完成。

例:

void read(Socket sock, OutputBuffer buff) {
sock.readsome([&] (Data data) {
buff.append(data);
sock.readsome(/*current lambda*/);
}).get();

}

从自身引用 lambda 的一种自然尝试是将其存储在变量中并通过引用捕获该变量:

auto on_read = [&] (Data data) {
buff.append(data);
sock.readsome(on_read);
};

但是,由于语义循环,这是不可能的:直到处理 lambda 表达式后才会推断出自动变量的类型,这意味着 lambda 表达式不能引用该变量。

另一种自然的方法是使用 std::function:

std::function on_read = [&] (Data data) {
buff.append(data);
sock.readsome(on_read);
};

这种方法可以编译,但通常会引入抽象损失:std::函数可能会产生内存分配,并且 lambda 的调用通常需要间接调用。

对于零开销解决方案,通常没有比显式定义本地类类型更好的方法了。

看来叮当是对的。考虑一个简化的示例:

auto it = [](auto& self) {
return [&self]() {
return self(self);
};
};
it(it);

让我们像编译器一样浏览它(一点):

  • it的类型与模板调用运算符Lambda1
  • it(it);触发调用运算符的实例化
  • 模板调用运算符的返回类型是auto,所以我们必须推导它。
  • 我们返回一个 lambda 捕获类型Lambda1的第一个参数。
  • 该 lambda 也有一个调用运算符,它返回调用的类型self(self)
  • 注意:self(self)正是我们开始的!

因此,无法推断类型。

好吧,你的代码不起作用。 但这确实:

template<class F>
struct ycombinator {
F f;
template<class...Args>
auto operator()(Args&&...args){
return f(f, std::forward<Args>(args)...);
}
};
template<class F>
ycombinator(F) -> ycombinator<F>;

测试代码:

ycombinator bob = {[x=0](auto&& self)mutable{
std::cout << ++x << "n";
ycombinator ret = {self};
return ret;
}};
bob()()(); // prints 1 2 3

您的代码既是 UB 又格式不正确,无需诊断。 这很有趣;但两者都可以独立修复。

首先,UB:

auto it = [&](auto self) { // outer
return [&](auto b) { // inner
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(5)(6);

这是 UB,因为 outer 按值获取self,然后 inner 通过引用捕获self,然后在outer完成运行后继续返回它。 所以段错误绝对没问题。

修复:

[&](auto self) {
return [self,&a](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};

代码仍然格式不正确。 要看到这一点,我们可以扩展 lambdas:

struct __outer_lambda__ {
template<class T>
auto operator()(T self) const {
struct __inner_lambda__ {
template<class B>
auto operator()(B b) const {
std::cout << (a + b) << std::endl;
return self(self);
}
int& a;
T self;
};
return __inner_lambda__{a, self};
}
int& a;
};
__outer_lambda__ it{a};
it(it);

这将实例化__outer_lambda__::operator()<__outer_lambda__>

template<>
auto __outer_lambda__::operator()(__outer_lambda__ self) const {
struct __inner_lambda__ {
template<class B>
auto operator()(B b) const {
std::cout << (a + b) << std::endl;
return self(self);
}
int& a;
__outer_lambda__ self;
};
return __inner_lambda__{a, self};
}
int& a;
};

所以我们接下来必须确定__outer_lambda__::operator()的返回类型 .

我们一行一行地浏览它。 首先,我们创建__inner_lambda__类型:

struct __inner_lambda__ {
template<class B>
auto operator()(B b) const {
std::cout << (a + b) << std::endl;
return self(self);
}
int& a;
__outer_lambda__ self;
};

现在,看看那里 - 它的返回类型是self(self)__outer_lambda__(__outer_lambda__ const&)。 但是我们正在尝试推断__outer_lambda__::operator()(__outer_lambda__)的返回类型。

你不被允许这样做。

虽然实际上__outer_lambda__::operator()(__outer_lambda__)的返回类型实际上并不依赖于__inner_lambda__::operator()(int)的返回类型,但C++在推导返回类型时并不关心;它只是逐行检查代码。

在我们推断之前self(self)就使用了它。 程序格式不正确。

我们可以通过隐藏self(self)来修补它,直到以后:

template<class A, class B>
struct second_type_helper { using result=B; };
template<class A, class B>
using second_type = typename second_type_helper<A,B>::result;
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [self,&a](auto b) {
std::cout << (a + b) << std::endl;
return self(second_type<decltype(b), decltype(self)&>(self) );
};
};
it(it)(4)(6)(42)(77)(999);
}

现在代码正确并编译。 但我认为这有点黑客;只需使用ycombinator即可。

根据编译器将为 lambda 表达式生成的类(或者更确切地说,应该)重写代码非常容易。

完成此操作后,很明显主要问题只是悬而未决的引用,并且不接受代码的编译器在lambda部门中受到一些挑战。

重写显示没有循环依赖项。

#include <iostream>
struct Outer
{
int& a;
// Actually a templated argument, but always called with `Outer`.
template< class Arg >
auto operator()( Arg& self ) const
//-> Inner
{
return Inner( a, self );    //! Original code has dangling ref here.
}
struct Inner
{
int& a;
Outer& self;
// Actually a templated argument, but always called with `int`.
template< class Arg >
auto operator()( Arg b ) const
//-> Inner
{
std::cout << (a + b) << std::endl;
return self( self );
}
Inner( int& an_a, Outer& a_self ): a( an_a ), self( a_self ) {}
};
Outer( int& ref ): a( ref ) {}
};
int main() {
int a = 5;
auto&& it = Outer( a );
it(it)(4)(6)(42)(77)(999);
}

完全模板化版本,用于反映原始代码中的内部 lambda 捕获模板化类型的项目的方式:

#include <iostream>
struct Outer
{
int& a;
template< class > class Inner;
// Actually a templated argument, but always called with `Outer`.
template< class Arg >
auto operator()( Arg& self ) const
//-> Inner
{
return Inner<Arg>( a, self );    //! Original code has dangling ref here.
}
template< class Self >
struct Inner
{
int& a;
Self& self;
// Actually a templated argument, but always called with `int`.
template< class Arg >
auto operator()( Arg b ) const
//-> Inner
{
std::cout << (a + b) << std::endl;
return self( self );
}
Inner( int& an_a, Self& a_self ): a( an_a ), self( a_self ) {}
};
Outer( int& ref ): a( ref ) {}
};
int main() {
int a = 5;
auto&& it = Outer( a );
it(it)(4)(6)(42)(77)(999);
}

我猜想,正是内部机器中的这种模板,正式规则旨在禁止。如果他们确实禁止原始结构。