【问题标题】:Lambda returning itself: is this legal?Lambda 自身返回:这合法吗?
【发布时间】:2025-12-05 12:45:01
【问题描述】:

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

#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()&lt;(lambda at lam.cpp:6:13)&gt;' 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 returning itself

【问题讨论】:

  • clang 此刻看起来更体面,我想知道这样的构造是否甚至可以进行类型检查,更有可能它最终会出现在无限树中。
  • 您询问是否合法,这表示这是一个语言律师问题,但有几个答案并没有真正采用这种方法......正确使用标签很重要
  • @ShafikYaghmour 谢谢,加了标签
  • @ArneVogel 是的,更新后的使用auto&amp; self 消除了悬空引用问题。
  • @TheGreatDuck C++ lambda 并不是真正的理论上的 lambda 表达式。 C++ 有内置的递归 types 原始的简单类型 lambda 演算无法表达,因此它可以具有与 a: a->a 和其他不可能的构造同构的东西。

标签: c++ lambda language-lawyer c++17 auto


【解决方案1】:

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

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

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

即使没有这个,你也有一个dangling reference


在与更聪明的人(即 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;
});

【讨论】:

  • 如何明确指定返回类型?我想不通。
  • @Rakete1111 哪一个?在原版中,你不能。
  • 哦,好的。我不是本地人,但“所以你必须明确提供返回类型”似乎暗示有一种方法,这就是我问的原因:)
  • @PedroA *.com/users/2756719/t-c 是 C++ 贡献者。他要么不是人工智能,要么足智多谋,足以说服一个同样了解 C++ 的人参加最近在芝加哥举行的 LWG 小型会议。
  • @Casey 或者人类只是在模仿人工智能告诉他的东西......你永远不知道;)
【解决方案2】:

编辑对于这种构造是否严格符合 C++ 规范似乎存在一些争议。普遍的观点似乎是它是无效的。请参阅其他答案以进行更彻底的讨论。此答案的其余部分适用于 如果 构造有效;下面的调整代码适用于 MSVC++ 和 gcc,并且 OP 已经发布了进一步修改的代码,也适用于 clang。

这是未定义的行为,因为内部 lambda 通过引用捕获参数 self,但 self 在第 7 行的 return 之后超出范围。因此,当稍后执行返回的 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

【讨论】:

  • 我不熟悉泛型 lambda,但您不能将 self 作为参考吗?
  • @FrançoisAndrieux 是的,如果你把self作为参考,这个问题goes away,但是Clang还是rejects it for another reason
  • @FrançoisAndrieux 确实,我已将其添加到答案中,谢谢!
  • 这种方法的问题在于它不能消除可能的编译器错误。所以也许它应该可以工作,但是实现被破坏了。
  • 谢谢,我已经看了好几个小时了,没有看到 self 被引用捕获!
【解决方案3】:

TL;DR;

clang 是正确的。

看起来标准中使这种格式不正确的部分是[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 Proposal to Add Y Combinator to the Standard Library,它提供了一个可行的解决方案:

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 对象。

它引用了dicussion in which Richard Smith alludes to the error that clang is giving you:

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

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

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

Barry 向我指出了后续提案 Recursive 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 表达式后才推断出 auto 变量的类型,这意味着 lambda 表达式无法引用该变量.

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

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

这种方法可以编译,但通常会引入抽象损失:std::function 可能会导致内存分配,而 lambda 的调用通常需要间接调用。

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

【讨论】:

  • @Cheersandhth.-Alf 我在阅读论文后最终找到了标准报价,所以它不相关,因为标准报价清楚地说明了为什么这两种方法都不起作用
  • ""如果表达式中出现具​​有未推断占位符类型的实体的名称,则程序格式错误"我在程序中没有看到这种情况。self似乎不是这样的实体。
  • @n.m.除了可能的措辞,这些例子似乎对措辞有意义,我相信这些例子清楚地说明了这个问题。我认为我目前无法添加更多内容来提供帮助。
【解决方案4】:

看来clang是对的。考虑一个简化的例子:

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

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

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

因此无法推断出类型。

【讨论】:

  • Lambda1::operator() 的返回类型是简单的Lambda2。然后在那个内部 lambda 表达式中,self(self) 的返回类型,Lambda1::operator() 的调用,已知也是Lambda2。可能正式规则阻碍了进行这种微不足道的推论,但这里提出的逻辑却没有。这里的逻辑只是一个断言。如果正式规则确实妨碍了,那么这就是正式规则的缺陷。
  • @Cheersandhth.-Alf 我同意返回类型是 Lambda2,但你知道你不能有一个未推断的呼叫运算符,因为这是你提议的:延迟扣除 Lambda2 的调用运算符返回类型。但是你不能改变这个规则,因为它是非常基本的。
【解决方案5】:

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

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);
  };
};

剩下的代码格式不正确。为了看到这一点,我们可以扩展 lambda:

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()&lt;__outer_lambda__&gt;:

  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&amp;)。但我们正在尝试推断__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。

【讨论】:

  • 可能(IDK)这个描述对于关于 lambdas 的正式规则是正确的。但是就模板重写而言,内部 lambda 的模板化 operator() 的返回类型通常在实例化之前无法推断(通过使用某种类型的某些参数调用)。所以a manual machine-like rewrite to template based code 工作得很好。
  • @cheers 您的代码不同; inner 是您代码中的模板类,但不在我或 OP 代码中。这很重要,因为模板类方法会延迟实例化,直到被调用。
  • 在模板函数中定义的类,等价于该函数之外的模板类。当演示代码具有模板化成员函数时,必须在函数外部定义它,因为 C++ 规则不允许本地用户定义类中的成员模板。这种形式上的限制不适用于编译器自己生成的任何东西。
【解决方案6】:

根据编译器将或应该为 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);
}

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

【讨论】:

  • 看,问题是template&lt; class &gt; class Inner;的模板operator()是...实例化的?嗯,用错词了。书面? ...在Outer::operator()&lt;Outer&gt; 期间推断外部运算符的返回类型之前。而Inner&lt;Outer&gt;::operator() 本身也调用了Outer::operator()&lt;Outer&gt;。这是不允许的。现在,大多数编译器不会注意到 self(self),因为他们等待在传入int 时推断Outer::Inner&lt;Outer&gt;::operator()&lt;int&gt; 的返回类型。明智的。但它忽略了代码的格式错误。
  • 嗯,我认为他们必须等到函数模板Innner&lt;T&gt;::operator()&lt;U&gt;被实例化后才能推断出函数模板的返回类型。毕竟返回类型可能取决于这里的U。它没有,但一般来说。
  • 确定;但是任何类型由不完整的返回类型推导确定的表达式仍然是非法的。只是有些编译器是懒惰的,直到以后才检查,到那时一切都可以正常工作。