C ++ 14中的递归lambda函数

时间:2013-08-06 16:14:28

标签: c++ lambda c++14

在C ++ 11中编写递归lambda函数有一个经常重复的“技巧”,如下所示:

std::function<int(int)> factorial;
factorial = [&factorial](int n)
{ return n < 2 ? 1 : n * factorial(n - 1); };

assert( factorial(5) == 120 );

(例如Recursive lambda functions in C++0x。)

此技术有两个直接的缺点:std::function<Sig>对象的目标(通过引用捕获)与非常特定的std::function<Sig>对象(此处为factorial)相关联。这意味着通常无法从函数返回生成的仿函数,否则引用将悬空。

另一个(尽管不那么直接)问题是std::function的使用通常会阻止编译器优化,这是在其实现中需要类型擦除的副作用。这不是假设,可以很容易地进行测试。

在假设的情况下,递归lambda表达式真的很方便,有没有办法解决这些问题?

2 个答案:

答案 0 :(得分:60)

问题的关键在于,在C ++ lambda表达式中,隐式 this参数将始终引用表达式的封闭上下文的对象(如果存在的话),并且不是lambda表达式产生的仿函数对象。

anonymous recursion借用一个叶子(有时也称为'open recursion'),我们可以使用C ++ 14的通用lambda表达式重新引入显式参数参考我们想要的递归函子:

auto f = [](auto&& self, int n) -> int
{ return n < 2 ? 1 : n * self(/* hold on */); };

调用者现在有一个新的负担,即调用表单,例如f(f, 5)。由于我们的lambda表达式是自引用的,它实际上是一个自己的调用者,因此我们应该return n < 2 ? 1 : n * self(self, n - 1);

由于在第一个位置显式传递仿函数对象本身的模式是可预测的,我们可以重构这个丑陋的疣:

template<typename Functor>
struct fix_type {
    Functor functor;

    template<typename... Args>
    decltype(auto) operator()(Args&&... args) const&
    { return functor(functor, std::forward<Args>(args)...); }

    /* other cv- and ref-qualified overloads of operator() omitted for brevity */
};

template<typename Functor>
fix_type<typename std::decay<Functor>::type> fix(Functor&& functor)
{ return { std::forward<Functor>(functor) }; }

这允许人们写:

auto factorial = fix([](auto&& self, int n) -> int
{ return n < 2 ? 1 : n * self(self, n - 1); });

assert( factorial(5) == 120 );

我们成功了吗?由于fix_type<F>对象包含它自己的函子,它为每次调用传递给它,因此永远不会存在悬空引用的风险。因此,我们的factorial对象可以毫无困难地无限复制,移入,移出和移出函数。

除了......虽然'外部'调用者可以随时调用factorial(5)形式,但事实证明,在我们的lambda表达式中,递归调用仍然看起来像self(self, /* actual interesting args */)。我们可以通过将fix_type更改为不将functor传递给自身,而是通过传递*this来改进此问题。也就是说,我们传入fix_type对象,该对象负责在第一个位置传递正确的“隐式显式”参数:return functor(*this, std::forward<Args>(args)...);。然后递归变为n * self(n - 1),应该是。

最后,这是main的生成代码,它使用return factorial(5);代替断言(对于fix_type的任何一种风格):

00000000004005e0 <main>:
  4005e0:       b8 78 00 00 00          mov    eax,0x78
  4005e5:       c3                      ret    
  4005e6:       66 90                   xchg   ax,ax

编译器能够优化所有内容,就像使用run-off-the-mill递归函数一样。


费用是多少?

精明的读者可能已经注意到一个奇怪的细节。在从非泛型lambda变为泛型lambda的过程中,我添加了一个显式返回类型(即-> int)。怎么样?

这与要推导的返回类型是条件表达式的类型这一事实有关,该类型取决于对self的调用,正在推导出哪种类型。快速阅读Return type deduction for normal functions表明重写lambda表达式如下:

[](auto&& self, int n)
{
    if(n < 2) return 1;               // return type is deduced here
    else return n * self(/* args */); // this has no impact
}

GCC实际上只接受第一种fix_type形式的代码(通过functor的代码)。我无法确定是否应该抱怨其他形式(通过*this)。我留给读者选择做出的权衡:减少类型推导,或者不那么难看的递归调用(当然也完全可以访问任何一种风格)。


GCC 4.9示例

答案 1 :(得分:11)

这不是lambda表达式,但几乎没有代码,适用于C ++ 98,可以递归:

struct {
    int operator()(int n) const {
        return n < 2 ? 1 : n * (*this)(n-1);
    }
} fact;
return fact(5);

根据[class.local]/1,它可以访问封闭函数可以访问的所有名称,这对于成员函数中的私有名称很重要。

当然,不是lambda,如果你想捕获函数对象之外的状态,你必须编写一个构造函数。