【问题标题】:Recursive lambda functions in C++11C++11 中的递归 lambda 函数
【发布时间】: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.stackoverflow.com/transcript/message/39298544#39298544
  • 仅供参考,在您的第二个代码 sn-p 中,您的 lambda 过于冗长,请考虑以下更改:std::function&lt;int(int,int)&gt; sum = [&amp;](int a, int b) {
  • 如果有人能够回答尾递归优化是否适用于任何解决方案,那将是受欢迎的。

标签: c++ c++11 lambda


【解决方案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 recursive{std::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;
    }
    

    【讨论】:

      猜你喜欢
      • 2015-10-07
      • 2013-08-07
      • 1970-01-01
      • 2010-11-07
      • 1970-01-01
      • 1970-01-01
      • 2013-03-29
      相关资源
      最近更新 更多