C++14 中的递归 lambda 函数

Posted

技术标签:

【中文标题】C++14 中的递归 lambda 函数【英文标题】:Recursive lambda functions in C++14 【发布时间】:2013-08-07 18:19:56 【问题描述】:

在 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&lt;Sig&gt; 对象的目标(通过引用捕获)绑定到一个非常特殊的std::function&lt;Sig&gt; 对象(这里是factorial)。这意味着生成的仿函数通常不能从函数返回,否则引用将悬空。

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

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

【问题讨论】:

嗯...显然不使用 lambda 表达式是这里的自然选择... 是的。编写一个普通的独立 C++ 函数;) @LucDanton:如果您选择用 C++ 编写递归 lambda,那么您选择 很糟糕。当您可以在函数中编写一个类时,没有理由这样做。是的,它会更乏味,但它仍然比递归 lambda 所需的体操更容易。 @LucDanton:真的吗? 大部分在支持组件中,但是需要编写 lambda 才能在该框架中工作,并且您必须在对 fix 的调用中使用它。并不是说这很可怕或其他什么,但这看起来好像 lambda 是新的 金锤,这是在手柄上添加一个螺丝刀头...... @dyp 那是this 【参考方案1】:

问题的症结在于,在 C++ lambda 表达式中,隐式 this 参数将始终引用表达式的封闭上下文的对象,如果存在的话,而不是由 lambda 表达式产生的函子对象。

借用anonymous 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 &lt; 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&lt;F&gt; 对象包含它自己的函子,它会在每次调用时传递给它,因此永远不会有悬空引用的风险。因此,我们的factorial 对象可以真正无休止地复制、移出、移入和移出函数。

除了...虽然“外部”调用者可以轻松地进行factorial(5) 形式的调用,但在我们的 lambda 表达式中,递归调用仍然看起来像self(self, /* actual interesting args */)。我们可以通过将fix_type 更改为不将functor 传递给自身,而是通过*this 来改进这一点。也就是说,我们传入fix_type 对象,该对象负责在第一个位置传递正确的“隐式即显式”参数:return functor(*this, std::forward&lt;Args&gt;(args)...);。然后递归变成n * self(n - 1),应该是这样。

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

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

编译器能够优化所有内容,就像使用普通的递归函数一样。


费用是多少?

精明的读者可能已经注意到一个奇怪的细节。在从非泛型到泛型 lambda 的转变中,我添加了一个显式返回类型(即-&gt; 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 示例

Complete code, first flavour Complete code, second flavour Complete code, first flavour, C++11 An example of a variadic fix for a group of mutually recursive lambda expressions

【讨论】:

展开并不难...一个成员函数odd,另一个even,然后operator() 进行初始调度(我必须承认我只是盲目猜测,我没有'不知道你所说的偶/奇函数 虽然它解释了为什么您一般而言不能轻松创建递归 lambda,但我不明白为什么它不允许用于简单的无捕获 lambda。毕竟,它们可以被认为是一个简单的函数,只是具有局部作用域。【参考方案2】:

它不是 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,如果要在函数对象之外捕获状态,则必须编写构造函数。

【讨论】:

老实说,这是递归 lambda 函数最实用的答案。 :) 世界上最好的答案

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

使用 C++14 lambda 测量任意函数的执行时间

C++C++中的lambda表达式和函数对象

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

python 函数中的递归lambda map reduce 等详解

C中的递归和返回语句

如何从 C++14 中的广义 lambda 捕获返回包含 std::unique_ptr 的 std::function?