C++14中的递归lambda函数

50

有一个经常被重复使用的“技巧”,可以在C++11中编写递归lambda函数,具体方法如下:

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

assert( factorial(5) == 120 );

(例如,C++0x中的递归lambda函数。)

然而,这种技术有两个缺点:首先,std::function<Sig>对象的目标(通过引用捕获)与一个非常特定的std::function<Sig>对象(此处为factorial)绑定。这意味着生成的函数对象通常无法从函数中返回,否则将留下悬空引用。

另一个问题(尽管不是立即显现的)是使用std::function通常会阻止编译器优化,这是在其实现中需要类型抹消的副作用。这不是假设性的问题,可以轻松地进行测试。

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


9
显然,在这里不使用lambda表达式是自然的选择... - David Rodríguez - dribeas
5
好的。编写一个普通的独立C++函数 ;) - n. m.
2
@LucDanton:如果你选择在C++中编写递归lambda函数,那么你的选择可能不太好。当你可以在函数内部编写一个类时,没有理由这样做。是的,这会更加繁琐,但它仍然比你需要进行的递归lambda函数要容易得多。 - Nicol Bolas
3
他们真的需要这样吗?它的大部分内容都在支持组件中,但 lambda 表达式需要编写以在该框架中工作,并且必须在调用“fix”时使用它。我并不是说这很糟糕或者什么的,但这看起来好像 lambda 是新的“万能工具”,而这只是在把螺丝刀头添加到手柄上。 - David Rodríguez - dribeas
2
@dyp 那个东西应该是这个 - RamblingMad
2个回答

68

问题的关键在于,在C++ lambda表达式中,如果存在,则隐式this参数始终将引用表达式所在上下文的对象,而不是lambda表达式生成的函数对象。

借鉴匿名递归(有时也称为“开放递归”)的思想,我们可以使用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对象可以自由地复制、移动、进出函数而毫不麻烦。

除了……在lambda表达式内部,递归调用仍然需要像self(self, /* actual interesting args */)这样的形式,而“外部”的调用方可以轻松地进行factorial(5)的调用。我们可以通过更改fix_type来改善这一点,不再将functor传递给自身,而是传递*this。也就是说,我们传递负责在第一个位置传递正确的“隐式作为显式”的参数的fix_type对象:return functor(*this, std::forward<Args>(args)...);。然后递归变成了n * self(n - 1),正如它应该的那样。

最后,这是使用return factorial(5);而不是断言(对于任何类型的fix_type)的main生成的代码:

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

编译器能够优化掉所有内容,就像处理普通递归函数一样。


成本是什么?

细心的读者可能会注意到一个奇怪的细节。在从非泛型 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示例


不是太难展开......一个成员函数叫做odd,另一个叫做even,然后是一个执行初始分派的operator()函数(我必须承认我只是盲目猜测,我不知道您所指的even/odd functions是什么)。 - David Rodríguez - dribeas
虽然它解释了为什么通常情况下你不能轻松地创建递归lambda表达式,但我不明白为什么对于简单的无捕获lambda表达式而言这是不允许的。毕竟,它们可以被视为一个简单的函数,只是具有局部作用域。 - Dan M.

17

虽然这不是一个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表达式那样,如果你想在函数对象之外捕获状态,你必须编写一个构造函数。

3
老实说,这是递归 Lambda 函数最实用的答案。 :) - vedranm
世界上最好的答案 - myworldbox

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接