C ++ 11中的递归Lambda函数


143

我是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:错误:' ((<lambda(int, int)>*)this)-><lambda(int, int)>::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);
};

有人可以对此进行说明吗?


这是静态声明还是隐式动态声明?
Hamish Grubijan 2010年

3
mutable那里的关键字是做什么的?
干杯和健康。-Alf

不允许使用非自动存储持续时间捕获变量。您应该这样操作:chat.stackoverflow.com/transcript/message/39298544#39298544
Euri Pinhollow

仅供参考,在您的第二个代码段中,您的lambda过于冗长,请考虑以下更改:std::function<int(int,int)> sum = [&](int a, int b) {
armanali

Answers:


189

考虑一下自动版本和完全指定的类型版本之间的区别。该自动关键字推断它的类型无论从那个它与初始化,但你与需要初始化它知道它的类型是什么什么(在这种情况下,拉姆达闭合需要知道它的捕获类型)。鸡和鸡蛋的问题。

另一方面,完全指定的函数对象的类型不需要“知道”任何有关为其分配的内容的信息,因此可以完全通知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中可以做到),只是它们与类型推断并不真正兼容。


3
我不同意这一点。输入函数体后,lambda的类型便广为人知-没有理由不应该在那时推断出它。
小狗

16
@DeadMG,但规范禁止引用auto其初始化程序中的变量。处理初始化程序时,尚不知道auto变量的类型。
Johannes Schaub-litb 2011年

1
想知道为什么它没有被标记为“答案”,而那个Python 1被分类为“答案”?
2013年

1
@Puppy:但是,在隐式捕获的情况下,为了提高效率,实际上仅捕获了引用的变量,因此必须对主体进行解析。
kec 2014年

sum除了之外std::function<int(int, int)>,是否还有其他有效的解释?或者C ++规范是否不介意推断它?
Mateen Ulhaq

79

诀窍是将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 ++ 11上时,它确实需要C ++ 14,但对于大多数人来说可能很有趣。

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


这不仅是关于C ++的特性,它还直接映射到Lambda微积分的数学上。从维基百科

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

3
这似乎比显式使用更糟糕function<>。我不明白为什么有人会喜欢它。编辑:显然更快。
Timmmm

17
这是方式比的std ::功能更好的为3个原因:它不需要类型擦除或内存分配,也可以是constexpr,并与自动(模板)参数/返回类型工作正常
伊万·桑斯,Carasa

3
大概此解决方案还具有在std :: function引用不超出范围的情况下可复制的优点?
乌里·格兰塔

3
嗯,在尝试时,GCC 8.1(linux)抱怨:error: use of ‘[...]’ before deduction of ‘auto’–需要明确指定返回类型(另一方面,不需要可变的)。
阿空加瓜

@Aconcagua在这里与Xcode10相同,我将C ++标准设置为17甚至
IceFire

39

使用C ++ 14,现在很容易std::function在仅几行代码中进行高效的递归lambda,而不必招致的额外开销(对原始代码进行少量编辑,以防止用户意外复制) ):

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.
        // [edit: Barry] pass in std::ref(*this) instead of *this
        return f(std::ref(*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) {
  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,则不需要演绎指南。


这很好,但是可以考虑std::forward<decltype(sum)>(sum)代替sum最后一行。
约翰·伦德伯格

@Johan不,只有一个人,operator()所以转发不会有任何好处sum
Barry

哦,是的。不习惯没有转发就使用转发引用。
约翰·伦德伯格

Y组合器无疑是必经之路。但是const,如果提供的功能对象具有非const调用运算符,则确实应该添加非重载。并使用SFINAE和noexcept两者进行计算。另外,C ++ 17中不再需要maker函数。
Deduplicator

2
@minex是的,auto sum复制了...但是它复制了一个reference_wrapper,这与引用一样。在实现中执行一次意味着没有任何用途会被意外复制。
巴里(Barry)

22

我有另一个解决方案,但仅适用于无状态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中的工作


3
这应该比std :: function具有更好的性能,因此替代方案为+1。但实际上,在这一点上,我想知道使用lambdas是否是最佳选择;)
Antoine 2014年

如果您有无状态的lambda,则也可以使其完全发挥作用。
Timmmm

1
@Timmmm但是随后您会将实现的一部分泄漏给外部单词,通常lambda与父函数紧密耦合(即使没有捕获)。如果不是这种情况,则不应首先使用lambda,而应使用函子的正常功能。
洋基队'18

10

可以递归调用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的范围。


3
但是,这与接受的答案相同,并且可能会因为使用std函数而受到惩罚。
约翰·隆德伯格

9

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

#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的结果类型。


6

我运行了一个基准测试,比较了使用std::function<>捕获方法的递归函数与递归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<>,至少在lambda捕获太大而无法容纳某些std::function小型函数库优化空间的情况下,其自身的性能可能是导致减速vs直接调用函数的原因。(我猜有点像各种短字符串优化吗?)。


2
-1。请注意,“ lambda”版本花费较长时间的唯一原因是因为将其绑定到std :: function,这使operator()调用了虚拟调用,这显然会花费更长的时间。最重要的是,在两种情况下,您在VS2012发布模式下的代码花费的时间大致相同。
Yam Marcovic

@YamMarcovic什么?这是当前编写递归lambda的唯一已知方法(这是示例的重点)。我很高兴得知VS2012找到了一种优化此用例的方法(尽管最近在该主题上有了更多的发展,显然,如果我的lambda捕获了更多的信息,那将不适合std :: function small-内存函子优化或其他)。
mmocny

2
已确认。我误解了你的帖子。+1。Gah,只有您编辑此答案才能投票。那么您能否再强调一点,例如在评论中?
Yam Marcovic

1
@YamMarcovic完成。感谢您愿意提供反馈并在需要时进行完善。+1,您好,先生。
mmocny

0时间通常表示“整个操作已被优化”。如果编译器证明您对计算的重新计算无能为力,则从cin中获取输入无济于事。
Yakk-Adam Nevraumont

1

这是fixpoint运算符的稍微简单的实现,这使正在发生的事情更加明显。

#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<int,int>,如果你使用std::function它需要从装箱新副本*this
Yankes 2014年

1

这是基于@Barry提出的Y组合器解决方案的改进版本。

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

它与let recOCaml中的关键字相似,尽管不相同。


0

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组合器解决方案

这是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

-1

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

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

是否可以使该答案编译器不可知?
rayryeng

-2

您正在尝试捕获定义中的变量(总和)。那不是很好。

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


3
但是如果将sum的声明从'auto'更改为std :: function <int(int,int)>而不更改捕获列表,它确实可以工作。
weima 2010年

因为它不再是lambda,而是可以代替lambda使用的功能?
Hamish Grubijan 2010年

-2

这个答案不如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但将允许自引用参数。
Yankes

我想发布一个没有结构的最小示例,可以随时编辑我的答案并提供更完整的解决方案。A struct也将增加一个间接级别。该示例有效,并且强制转换符合标准,我不知道它的-1用途。
user1095108 2014年

不,struct将仅用作指针的容器,并将作为值传递。这将不会比指针更多的间接或开销。关于-1我不知道是谁给你的,但是我认为它是因为reinterpret_cast应该将其作为最后的手段。
Yankes

cast由C ++ 11标准假想保证工作。struct在我眼中,使用a 可能会不利于使用lambda对象。毕竟,struct您建议的是一个使用lambda对象的函子。
user1095108 2014年

查看@Pseudonym解决方案,仅std::function将其删除,您将获得与我所想的接近的东西。这可能与您的解决方案具有相似的性能。
扬克斯2014年

-3

您需要一个定点组合器。看到这个

或查看以下代码:

//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;
}
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.