Lambda返回自己:这合法吗?

时间:2018-09-05 19:42:51

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

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

#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或都不存在?

更新:叮当声接受了此轻微修改:

  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

6 个答案:

答案 0 :(得分:69)

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

  

如果表达式中出现具​​有未推导占位符类型的实体的名称,则程序格式错误。但是,一旦在函数中看到未丢弃的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;
});

答案 1 :(得分:35)

编辑 根据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以使其通过引用而不是通过值来获取自身,从而避免了一堆不必要的复制并解决了问题:

#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

答案 2 :(得分:21)

TL; DR;

c是正确的。

看起来该格式不正确的部分是[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
}
     

-结束示例]

原始工作通过

如果我们查看提案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

  

我认为作为一流的语言功能会更好。我在科纳会议前的会议上没有时间了,但是我打算写一篇论文,让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表达式处理后才能推导出,这意味着lambda表达式无法引用该变量

     

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

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

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

     

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

答案 3 :(得分:13)

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

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

让我们像编译器一样仔细地研究它:

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

因此,无法推断类型。

答案 4 :(得分: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。

答案 5 :(得分: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);
}

我想这就是内部机制中的模板,正式规则是禁止使用的。如果他们确实禁止使用原始结构。