C++11 中的递归 lambda 函数

Posted

技术标签:

【中文标题】C++11 中的递归 lambda 函数【英文标题】:Recursive lambda functions in C++11 【发布时间】:2011-01-05 07:05:47 【问题描述】:

我是 C++11 的新手。我正在编写以下递归 lambda 函数,但它无法编译。

sum.cpp

#include <iostream>
#include <functional>

auto term = [](int a)->int 
  return a*a;
;

auto next = [](int a)->int 
  return ++a;
;

auto sum = [term,next,&sum](int a, int b)mutable ->int 
  if(a>b)
    return 0;
  else
    return term(a) + sum(next(a),b);
;

int main()
  std::cout<<sum(1,10)<<std::endl;
  return 0;

编译错误:

vimal@linux-718q:~/Study/09C++/c++0x/lambda> g++ -std=c++0x sum.cpp

sum.cpp:在 lambda 函数中: sum.cpp:18:36: 错误:'((&lt;lambda(int, int)&gt;*)this)-&gt;&lt;lambda(int, int)&gt;::sum' 不能用作函数

gcc 版本

gcc 版本 4.5.0 20091231(实验性)(GCC)

但是,如果我将sum() 的声明更改如下,它会起作用:

std::function<int(int,int)> sum = [term,next,&sum](int a, int b)->int 
   if(a>b)
     return 0;
   else
     return term(a) + sum(next(a),b);
;

有人可以解释一下吗?

【问题讨论】:

这可能是静态声明还是隐式动态声明? mutable 关键字在那里做什么? 不允许捕获具有非自动存储持续时间的变量。你应该这样做:chat.***.com/transcript/message/39298544#39298544 仅供参考,在您的第二个代码 sn-p 中,您的 lambda 过于冗长,请考虑以下更改:std::function&lt;int(int,int)&gt; sum = [&amp;](int a, int b) 如果有人能够回答尾递归优化是否适用于任何解决方案,那将是受欢迎的。 【参考方案1】:

想想 auto 版本和完全指定类型版本之间的区别。 auto 关键字从它初始化的任何内容中推断出它的类型,但是你初始化它需要知道它的类型是什么(在这种情况下,lambda 闭包需要知道它正在捕获的类型) .有点像先有鸡还是先有蛋的问题。

另一方面,完全指定的函数对象的类型不需要“知道”任何关于分配给它的内容,因此 lambda 的闭包同样可以完全了解其捕获的类型。

考虑一下对代码的这种轻微修改,它可能更有意义:

std::function<int(int,int)> sum;
sum = [term,next,&sum](int a, int b)->int 
if(a>b)
    return 0;
else
    return term(a) + sum(next(a),b);
;

显然,这不适用于 auto。递归 lambda 函数运行良好(至少它们在 MSVC 中运行良好,我有使用它们的经验),只是它们与类型推断并不真正兼容。

【讨论】:

我不同意这一点。一旦输入函数体,lambda 的类型就众所周知 - 没有理由不应该在那时推导它。 @DeadMG 但规范禁止在其初始化程序中引用 auto 变量。处理初始化程序时,auto 变量的类型尚不清楚。 想知道为什么它没有被标记为“答案”,而 Python 被归类为“答案”?! @Puppy:但是,在隐式捕获的情况下,为了提高效率,实际上只捕获引用的变量,因此必须解析主体。 除了std::function&lt;int(int, int)&gt; 之外,sum 是否有有效的解释,或者 C++ 规范只是懒得推断?【参考方案2】:

诀窍是将 lambda 实现作为参数提供给自身作为参数,而不是通过捕获。

const auto sum = [term,next](int a, int b) 
  auto sum_impl=[term,next](int a,int b,auto& sum_ref) mutable 
    if(a>b)
      return 0;
    
    return term(a) + sum_ref(next(a),b,sum_ref);
  ;
  return sum_impl(a,b,sum_impl);
;

计算机科学中的所有问题都可以通过另一个间接层次来解决。我首先在http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/ 发现了这个简单的技巧

确实需要 C++14,而问题是关于 C++11,但可能对大多数人来说很有趣。

通过std::function 也是可能的,但可能会导致代码变慢。但不总是。看看std::function vs template的答案


这不仅仅是 C++ 的一个特点, 它直接映射到 lambda 演算的数学。来自Wikipedia:

Lambda calculus cannot express this as directly as some other notations:
all functions are anonymous in lambda calculus, so we can't refer to a
value which is yet to be defined, inside the lambda term defining that
same value. However, recursion can still be achieved by arranging for a
lambda expression to receive itself as its argument value

【讨论】:

这似乎比明确使用function&lt;&gt; 更糟糕。我不明白为什么有人会喜欢它。编辑:显然更快。 这比 std::function 好得多,原因有 3 个:它不需要类型擦除或内存分配,它可以是 constexpr 并且它可以与自动(模板化)参数/返回类型一起正常工作 大概这个解决方案还具有可复制的优点,而 std::function 引用不会超出范围? 嗯,尝试时,GCC 8.1 (linux) 抱怨:error: use of ‘[...]’ before deduction of ‘auto’ - 需要明确指定返回类型(另一方面,不需要可变)。 @JohanLundberg 它仅在函数中有另一个返回时才有效(因此可以推断返回类型)-在示例中已经有一个 return 0 因此编译器可以推断出返回类型是int -- 一般情况下需要指定返回类型。【参考方案3】:

使用 C++14,现在可以很容易地制作一个高效的递归 lambda,而无需产生 std::function 的额外开销,只需几行代码:

template <class F>
struct y_combinator 
    F f; // the lambda will be stored here
    
    // a forwarding operator():
    template <class... Args>
    decltype(auto) operator()(Args&&... args) const 
        // we pass ourselves to f, then the arguments.
        return f(*this, std::forward<Args>(args)...);
    
;

// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f) 
    return std::forward<F>(f);

你原来的sum尝试变成了:

auto sum = make_y_combinator([term,next](auto sum, int a, int b) -> int 
  if (a>b) 
    return 0;
  
  else 
    return term(a) + sum(next(a),b);
  
);

在C++17中,通过CTAD,我们可以添加一个推导指南:

template <class F> y_combinator(F) -> y_combinator<F>;

这消除了对辅助函数的需要。我们可以直接写y_combinator[](auto self, ...)...


在 C++20 中,使用 CTAD 进行聚合,不需要推导指南。


在 C++23 中,通过推断这一点,您根本不需要 Y 组合器:

auto sum = [term,next](this auto const& sum, int a, int b) -> int 
  if (a>b) 
    return 0;
  
  else 
    return term(a) + sum(next(a),b);
  

【讨论】:

Y-combinator 肯定是要走的路。但是你真的应该添加一个非const 重载,以防提供的函数对象有一个非const 调用操作符。并为两者使用 SFINAE 和计算 noexcept。此外,C++17 中不再需要 maker-function。 @minex 是的,auto sum 复制...但它复制了 reference_wrapper,这与获取参考相同。在实现中执行一次意味着不会意外复制任何用途。 不知道为什么,但是貌似我的lambda要加上-&gt;void返回类型信息,不然编译失败:godbolt.org/z/WWj14P @qbolec 编译器需要知道它返回什么,并且没有 return 来提示它,所以有时你只需要提供它(即使在这种情况下它应该是“显然”@ 987654337@) @Barry,你所说的可能是故事的一部分,但肯定还有更多内容,因为在函数中添加return 42; 似乎还不够——它仍然需要-&gt; int:@ 987654322@【参考方案4】:

我有另一种解决方案,但仅适用于无状态 lambda:

void f()

    static int (*self)(int) = [](int i)->int  return i>0 ? self(i-1)*i : 1; ;
    std::cout<<self(10);

这里的诀窍是 lambda 可以访问静态变量,您可以将无状态变量转换为函数指针。

您可以将其与标准 lambda 一起使用:

void g()

    int sum;
    auto rec = [&sum](int i) -> int
    
        static int (*inner)(int&, int) = [](int& _sum, int i)->int 
        
            _sum += i;
            return i>0 ? inner(_sum, i-1)*i : 1; 
        ;
        return inner(sum, i);
    ;

它在 GCC 4.7 中的工作

【讨论】:

这应该比 std::function 有更好的性能,所以 +1 可以替代。但实际上,在这一点上,我想知道使用 lambdas 是否是最好的选择;) 如果你有一个无状态的 lambda,你也可以让它成为一个完整的函数。 @Timmmm 但是你将部分实现泄露给外界,通常 lambdas 与父函数紧密耦合(即使没有捕获)。如果不是这种情况,那么您不应该首先使用 lambda,而应使用函子的普通函数。【参考方案5】:

要使 lambda 递归而不使用外部类和函数(如 std::function 或定点组合器),可以在 C++14 (live example) 中使用以下构造:

#include <utility>
#include <list>
#include <memory>
#include <iostream>

int main()

    struct tree
    
        int payload;
        std::list< tree > children = ; // std::list of incomplete type is allowed
    ;
    std::size_t indent = 0;
    // indication of result type here is essential
    const auto print = [&] (const auto & self, const tree & node) -> void
    
        std::cout << std::string(indent, ' ') << node.payload << '\n';
        ++indent;
        for (const tree & t : node.children) 
            self(self, t);
        
        --indent;
    ;
    print(print, 1, 2, 8, 3, 5, 7, 6, 4);

打印:

1
 2
  8
 3
  5
   7
  6
 4

注意,lambda 的结果类型应该明确指定。

【讨论】:

这里唯一看起来有用的答案。 这实际上与将 lambda 本身作为参数传递相同。你怎么看不到@JohanLundberg 的帖子上面的帖子?【参考方案6】:

可以让 lambda 函数以递归方式调用自身。您唯一需要做的就是通过函数包装器引用它,以便编译器知道它的返回值和参数类型(您无法捕获尚未定义的变量——lambda 本身) .

  function<int (int)> f;

  f = [&f](int x) 
    if (x == 0) return 0;
    return x + f(x-1);
  ;

  printf("%d\n", f(10));

小心不要超出包装器 f 的范围。

【讨论】:

但是,这与接受的答案相同,并且可能会因使用 std 函数而受到惩罚。【参考方案7】:

我使用std::function&lt;&gt; 捕获方法运行了一个比较递归函数与递归 lambda 函数的基准测试。在 clang 版本 4.1 上启用全面优化后,lambda 版本的运行速度明显变慢。

#include <iostream>
#include <functional>
#include <chrono>

uint64_t sum1(int n) 
  return (n <= 1) ? 1 : n + sum1(n - 1);


std::function<uint64_t(int)> sum2 = [&] (int n) 
  return (n <= 1) ? 1 : n + sum2(n - 1);
;

auto const ITERATIONS = 10000;
auto const DEPTH = 100000;

template <class Func, class Input>
void benchmark(Func&& func, Input&& input) 
  auto t1 = std::chrono::high_resolution_clock::now();
  for (auto i = 0; i != ITERATIONS; ++i) 
    func(input);
  
  auto t2 = std::chrono::high_resolution_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
  std::cout << "Duration: " << duration << std::endl;


int main() 
  benchmark(sum1, DEPTH);
  benchmark(sum2, DEPTH);

产生结果:

Duration: 0 // regular function
Duration: 4027 // lambda function

(注意:我还确认了一个从 cin 获取输入的版本,以消除编译时评估)

Clang 还会产生编译器警告:

main.cc:10:29: warning: variable 'sum2' is uninitialized when used within its own initialization [-Wuninitialized]

这是预期的,也是安全的,但应该注意。

很高兴在我们的工具带中提供解决方案,但我认为如果性能要与当前方法相媲美,该语言将需要一种更好的方法来处理这种情况。

注意:

正如评论者所指出的,最新版本的 VC++ 似乎已经找到了一种将其优化到同等性能的方法。毕竟,也许我们不需要更好的方法来处理这个问题(语法糖除外)。

此外,正如最近几周其他一些 SO 帖子所概述的那样,std::function&lt;&gt; 本身的性能可能是导致速度变慢的原因而不是直接调用函数,至少当 lambda 捕获太大而无法适应某些库优化时空间std::function 用于小型函子(我猜有点像各种短字符串优化?)。

【讨论】:

-1。请注意,“lambda”版本需要更长时间的唯一原因是因为您将它绑定到 std::function,这使得 operator() 调用虚拟调用,这显然需要更长的时间。最重要的是,在 VS2012 发布模式下,您的代码在两种情况下花费的时间大致相同。 @YamMarcovic 什么?这是目前唯一已知的编写递归 lambda 的方法(这就是示例的重点)。我很高兴知道 VS2012 已经找到了一种优化这个用例的方法(尽管最近在这个主题上有更多的发展,显然如果我的 lambda 捕获了更多它就不适合 std::function small-内存函子优化或诸如此类)。 已确认。我误解了你的帖子。然后+1。 Gah,只有在您编辑此答案时才能投票。那么您能否再强调一下,比如在评论中? @YamMarcovic 完成。感谢您愿意提供反馈并在需要时对其进行改进。 +1 给你,好先生。 0 时间通常意味着“整个操作被优化掉了”。如果编译器证明你对计算结果没有做任何事情,那么从 cin 获取输入什么也做不了。【参考方案8】:

这是基于@Barry 提出的 Y-combinator 解决方案的改进版本。

template <class F>
struct recursive 
  F f;
  template <class... Ts>
  decltype(auto) operator()(Ts&&... ts)  const  return f(std::ref(*this), std::forward<Ts>(ts)...); 

  template <class... Ts>
  decltype(auto) operator()(Ts&&... ts)   return f(std::ref(*this), std::forward<Ts>(ts)...); 
;

template <class F> recursive(F) -> recursive<F>;
auto const rec = [](auto f) return recursivestd::move(f); ;

要使用它,可以执行以下操作

auto fib = rec([&](auto&& fib, int i) 
// implementation detail omitted.
);

它类似于OCaml中的let rec关键字,虽然不一样。

【讨论】:

这个答案至少需要 C++17。否则我会得到:error: expected constructor, destructor, or type conversion before ‘;’ token in line template &lt;class F&gt; recursive(F) -&gt; recursive&lt;F&gt;;【参考方案9】:

这是定点运算符的一个稍微简单的实现,这使得它更清楚到底发生了什么。

#include <iostream>
#include <functional>

using namespace std;

template<typename T, typename... Args>
struct fixpoint

    typedef function<T(Args...)> effective_type;
    typedef function<T(const effective_type&, Args...)> function_type;

    function_type f_nonr;

    T operator()(Args... args) const
    
        return f_nonr(*this, args...);
    

    fixpoint(const function_type& p_f)
        : f_nonr(p_f)
    
    
;


int main()

    auto fib_nonr = [](const function<int(int)>& f, int n) -> int
    
        return n < 2 ? n : f(n-1) + f(n-2);
    ;

    auto fib = fixpoint<int,int>(fib_nonr);

    for (int i = 0; i < 6; ++i)
    
        cout << fib(i) << '\n';
    

【讨论】:

我认为如果您将std::function 替换为函数指针(在内核中,它仅适用于普通函数和无状态 lambda),您可以改进您的答案(性能方面)。顺便说一句,fib_nonr 应该接受fixpoint&lt;int,int&gt;,如果你使用std::function,它需要从*this 创建新副本。【参考方案10】:

C++ 14: 这是一个递归匿名无状态/不捕获通用 lambda 集 输出 1, 20 中的所有数字

([](auto f, auto n, auto m) 
    f(f, n, m);
)(
    [](auto f, auto n, auto m) -> void

    cout << typeid(n).name() << el;
    cout << n << el;
    if (n<m)
        f(f, ++n, m);
,
    1, 20);

如果我理解正确,这是使用 Y-combinator 解决方案

这里是 sum(n, m) 版本

auto sum = [](auto n, auto m) 
    return ([](auto f, auto n, auto m) 
        int res = f(f, n, m);
        return res;
    )(
        [](auto f, auto n, auto m) -> int
        
            if (n > m)
                return 0;
            else 
                int sum = n + f(f, n + 1, m);
                return sum;
            
        ,
        n, m); ;

auto result = sum(1, 10); //result == 55

【讨论】:

【参考方案11】:

您正在尝试捕获正在定义的变量(总和)。这可不好。

我认为真正的自递归 C++0x lambda 是不可能的。不过,您应该能够捕获其他 lambda。

【讨论】:

但如果 sum 的声明从 'auto' 更改为 std::function 而不更改捕获列表,它确实有效。 因为那时它不再是一个lambda,而是一个可以用来代替lambda的函数?【参考方案12】:

这是 OP 的最终答案。无论如何,Visual Studio 2010 不支持捕获全局变量。而且您不需要捕获它们,因为全局变量可以通过定义全局访问。以下答案改为使用局部变量。

#include <functional>
#include <iostream>

template<typename T>
struct t2t

    typedef T t;
;

template<typename R, typename V1, typename V2>
struct fixpoint

    typedef std::function<R (V1, V2)> func_t;
    typedef std::function<func_t (func_t)> tfunc_t;
    typedef std::function<func_t (tfunc_t)> yfunc_t;

    class loopfunc_t 
    public:
        func_t operator()(loopfunc_t v)const 
            return func(v);
        
        template<typename L>
        loopfunc_t(const L &l):func(l)
        typedef V1 Parameter1_t;
        typedef V2 Parameter2_t;
    private:
        std::function<func_t (loopfunc_t)> func;
    ;
    static yfunc_t fix;
;
template<typename R, typename V1, typename V2>
typename fixpoint<R, V1, V2>::yfunc_t fixpoint<R, V1, V2>::fix = [](tfunc_t f) -> func_t 
    return [f](fixpoint<R, V1, V2>::loopfunc_t x)  return f(x(x)); 
    ([f](fixpoint<R, V1, V2>::loopfunc_t x) -> fixpoint<R, V1, V2>::func_t
        auto &ff = f;
        return [ff, x](t2t<decltype(x)>::t::Parameter1_t v1, 
            t2t<decltype(x)>::t::Parameter1_t v2)
            return ff(x(x))(v1, v2);
        ; 
    );
;

int _tmain(int argc, _TCHAR* argv[])

    auto term = [](int a)->int 
      return a*a;
    ;

    auto next = [](int a)->int 
      return ++a;
    ;

    auto sum = fixpoint<int, int, int>::fix(
    [term,next](std::function<int (int, int)> sum1) -> std::function<int (int, int)>
        auto &term1 = term;
        auto &next1 = next;
        return [term1, next1, sum1](int a, int b)mutable ->int 
            if(a>b)
                return 0;
        else
            return term1(a) + sum1(next1(a),b);
        ;
    );

    std::cout<<sum(1,10)<<std::endl; //385

    return 0;

【讨论】:

是否有可能使这个答案编译器不可知?【参考方案13】:

这个答案不如 Yankes 的答案,但还是这样:

using dp_type = void (*)();

using fp_type = void (*)(dp_type, unsigned, unsigned);

fp_type fp = [](dp_type dp, unsigned const a, unsigned const b) 
  ::std::cout << a << ::std::endl;
  return reinterpret_cast<fp_type>(dp)(dp, b, a + b);
;

fp(reinterpret_cast<dp_type>(fp), 0, 1);

【讨论】:

我认为你应该避免使用reinterpret_cast。在您的情况下,可能最好的方法是创建一些替换dp_type 的结构。它应该有字段fp_type,可以从fp_type 构造,并有运算符()fp_type 之类的参数。这将接近std::function,但允许自引用参数。 我想发布一个没有结构的最小示例,请随时编辑我的答案并提供更完整的解决方案。 struct 还会增加一个额外的间接级别。该示例有效,并且演员表符合标准,我不知道-1 的用途。 不,struct 只能用作指针的容器,并将作为值传递。这不会比指针更多的间接或开销。还有关于-1我不知道是谁给你的,但我认为这是因为reinterpret_cast应该作为最后的手段。 cast 应该保证在 c++11 标准下工作。在我看来,使用 struct 可能会打败 lambda 对象的使用。毕竟,您提出的struct 是一个仿函数,利用了一个 lambda 对象。 查看@Pseudonym 解决方案,仅删除std::function,您将得到与我的想法相近的东西。这可能与您的解决方案具有相似的性能。【参考方案14】:

您需要一个定点组合器。见this。

或者看下面的代码:

//As decltype(variable)::member_name is invalid currently, 
//the following template is a workaround.
//Usage: t2t<decltype(variable)>::t::member_name
template<typename T>
struct t2t

    typedef T t;
;

template<typename R, typename V>
struct fixpoint

    typedef std::function<R (V)> func_t;
    typedef std::function<func_t (func_t)> tfunc_t;
    typedef std::function<func_t (tfunc_t)> yfunc_t;

    class loopfunc_t 
    public:
        func_t operator()(loopfunc_t v)const 
            return func(v);
        
        template<typename L>
        loopfunc_t(const L &l):func(l)
        typedef V Parameter_t;
    private:
        std::function<func_t (loopfunc_t)> func;
    ;
    static yfunc_t fix;
;
template<typename R, typename V>
typename fixpoint<R, V>::yfunc_t fixpoint<R, V>::fix = 
[](fixpoint<R, V>::tfunc_t f) -> fixpoint<R, V>::func_t 
    fixpoint<R, V>::loopfunc_t l = [f](fixpoint<R, V>::loopfunc_t x) ->
        fixpoint<R, V>::func_t
            //f cannot be captured since it is not a local variable
            //of this scope. We need a new reference to it.
            auto &ff = f;
            //We need struct t2t because template parameter
            //V is not accessable in this level.
            return [ff, x](t2t<decltype(x)>::t::Parameter_t v)
                return ff(x(x))(v); 
            ;
        ; 
        return l(l);
    ;

int _tmain(int argc, _TCHAR* argv[])

    int v = 0;
    std::function<int (int)> fac = 
    fixpoint<int, int>::fix([](std::function<int (int)> f)
        -> std::function<int (int)>
        return [f](int i) -> int
            if(i==0) return 1;
            else return i * f(i-1);
        ;
    );

    int i = fac(10);
    std::cout << i; //3628800
    return 0;

【讨论】:

以上是关于C++11 中的递归 lambda 函数的主要内容,如果未能解决你的问题,请参考以下文章

Python lambda匿名函数,递归应用

day4之内置函数匿名函数,递归函数

Python:lambda表达式和yield关键字理解与使用讲解

为啥 c++11 中的 lambda 函数没有 f​​unction<> 类型?

aws lambda 函数可以向专用网​​络中的端点发布帖子吗?

lambda啥意思