Lambda回归自我:这合法吗?


124

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

#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返回自身


2
当下clang看起来更像样,我想知道这样的构造是否可以进行类型检查,更有可能最终出现在无限树中。
bipll

2
您问这是否合法,这是一个语言律师问题,但有几个答案并没有真正采用该方法……重要的是要正确
Shafik Yaghmour

2
@ShafikYaghmour谢谢,加上一个标签
n。代词

1
@ArneVogel是的,更新后的版本使用auto& self它消除了悬而未决的参考问题。
n。代词

1
@TheGreatDuck C ++ lambda并不是真正的理论lambda表达式。C ++具有内置的递归类型,原始的简单类型的lambda演算无法表达这些内置的递归类型,因此它可以具有与a同构的东西:a-> a和其他不可能的构造。
n。代词

Answers:


68

根据[dcl.spec.auto] / 9,程序格式错误((右):

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

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

即使没有这些,您也有悬而未决的参考文献


在与更聪明的人(例如TC)讨论之后,让我详细说明一下:原始代码(略有减少)和建议的新版本(也有所减少)之间存在重要区别:

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)可以尝试实例化此实例。您知道外层lambda的推导类型,一旦您到达上面的;点即可#2(这是内层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'9

@ Rakete1111哪一个?在原始版本中,您不能。
巴里(Barry)

哦好的。我不是本地人,但是“所以您必须显式提供返回类型”似乎暗示有一种方法,这就是为什么我要问这个问题:)
Rakete1111

4
@PedroA stackoverflow.com/users/2756719/tc是C ++贡献者。他还是不是 AI专家,还是足够有才干的人说服一个也懂C ++的人参加最近在芝加哥举行的LWG小型会议。
凯西

3
@Casey也许人类只是在模仿AI告诉他的话...你永远不会知道;)
TC

34

编辑关于这种构造是否严格按照C ++规范有效,似乎存在一些争议。普遍认为这是无效的。请参阅其他答案,以进行更彻底的讨论。如果构造有效,其余答案适用。下面经过调整的代码可与MSVC ++和gcc一起使用,并且OP发布了进一步修改的代码,也可与clang一起使用。

这是未定义的行为,因为内部lambda self通过引用捕获参数,但是在第7行self之后超出范围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以使其通过引用而不是通过值获取自身,从而避免了一堆不必要的复制并解决了以下问题:

#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提供参考,此问题就消失了,但是Clang仍然出于其他原因拒绝了它
Justin

@FrançoisAndrieux确实,我已经将其添加到了答案中,谢谢!
TypeIA

这种方法的问题在于它不能消除可能的编译器错误。因此,也许它应该可以工作,但是实现被破坏了。
Shafik Yaghmour '18

谢谢,我已经看了好几个小时了,没发现self是参考资料!
n。代词

21

TL; DR;

lang是正确的。

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

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

—结束示例]

原创作品

如果我们查看提案“将Y组合器添加到标准库中的提案”,它将提供一个可行的解决方案:

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给您的错误

我认为作为一流的语言功能会更好。我在科纳之前的会议上没有时间了,但是我打算写一篇论文,让lambda取一个名字(视其自身而定):

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

在这里,“ fib”与lambda的* this等效(尽管lambda的闭包类型不完整,但有些恼人的特殊规则允许它工作)。

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在阅读论文后,我最终找到了标准报价,因此它没有意义,因为标准报价明确说明了两种方法都不起作用的原因
Shafik Yaghmour

“”如果有undeduced占位符类型出现在表达式中的实体的名称,该方案是形成不良”我没有看到这方面的一个发生在节目中虽然。self似乎没有这样的一个实体。
ñ。 '代词

@nm除了可能的措辞外,这些示例似乎在措辞上也是有意义的,我相信这些示例可以清楚地说明问题。我认为我目前无法提供更多帮助。
Shafik Yaghmour

13

似乎c是正确的。考虑一个简化的示例:

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

让我们像编译器一样(有点)来经历它:

  • 该类型itLambda1一个模板调用操作。
  • it(it); 触发呼叫操作员的实例化
  • 模板调用运算符的返回类型为auto,因此我们必须推断出它。
  • 我们将返回一个lambda来捕获type的第一个参数Lambda1
  • 该lambda也具有一个调用操作符,该操作符返回调用的类型 self(self)
  • 注意:self(self)这正是我们开始的目的!

因此,无法推断类型。


的返回类型Lambda1::operator()Lambda2。然后,在该内部lambda表达式self(self)Lambda1::operator(),也称为的调用的返回类型也是Lambda2。可能,形式规则阻碍了这种微不足道的推论,但是这里提出的逻辑却没有。这里的逻辑仅是一个断言。如果正式规则确实妨碍了您,那么这就是正式规则中的缺陷。
干杯和健康。-Alf

@ Cheersandhth.-Alf我同意返回类型为Lambda2,但是您确实知道您不能拥有一个未推论的呼叫运算符,因为这是您的建议:延迟Lambda2的呼叫运算符返回类型的推导。但是您不能更改其规则,因为这是非常基本的。
Rakete1111'9

9

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

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,因为外部self按值取值,然后内部self按引用捕获,然后在outer完成运行后继续将其返回。因此,segfaulting绝对可以。

解决方法:

[&](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()<__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。


可能(IDK),此描述对于有关lambda的正式规则是正确的。但是就模板重写而言,内部lambda的templated的返回类型operator()通常无法实例化(通过使用某种类型的参数调用),才能推导出。因此,将类似机器的手动重写为基于模板的代码非常有效。
干杯和健康。

@cheers您的代码不同;inner是您代码中的模板类,但不在我或OP代码中。这很重要,因为模板类方法被延迟实例化直到被调用。
Yakk-Adam Nevraumont

在模板化函数内定义的类等效于该函数外的模板化类。当演示代码具有模板成员函数时,必须在函数外部对其进行定义,因为C ++规则不允许在本地用户定义的类中使用成员模板。正式的限制并不适用于编译器自己生成的任何内容。
干杯和健康。-Alf

7

根据编译器将为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< class > class Inner;的模板operator()已实例化?好吧,错字。书面?...在Outer::operator()<Outer>推断外部运算符的返回类型之前。并Inner<Outer>::operator()呼吁Outer::operator()<Outer>自己。那是不允许的。现在,大多数编译器没有注意到self(self)因为它们等待推断Outer::Inner<Outer>::operator()<int>何时int传入的返回类型。明智的。但是它错过了代码的不良形式。
Yakk-Adam Nevraumont

好吧,我认为他们必须等待推断出功能模板的返回类型,直到Innner<T>::operator()<U>实例化该功能模板。毕竟,返回类型可能取决于U此处。没有,但是总的来说。
干杯和健康。

当然; 但是任何类型由不完全的返回类型推导确定的表达式仍然是非法的。只是有些编译器比较懒惰,直到以后才检查,这时everinging可以正常工作。
Yakk-Adam Nevraumont
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.