C++14中的递归lambda函数
Recursive lambda functions in 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 );
(例如C++0x中的递归lambda函数。)
然而,这种技术有两个直接的缺点:std::function<Sig>
对象的目标(通过引用捕获)与非常特定的std::function<Sig>
对象(此处为factorial
)绑定。这意味着生成的函子通常不能从函数中返回,否则引用将处于悬空状态。
另一个(虽然不那么直接)问题是,std::function
的使用通常会阻止编译器优化,这是其实现中需要类型擦除的副作用。这不是假设性的,可以很容易地进行测试。
在递归lambda表达式非常方便的假设情况下,有没有办法解决这些问题?
问题的关键在于,在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
对象可以无休止地复制、从函数中移动、进出函数,而不会带来麻烦。
除了。。。虽然"外部"调用方可以很容易地进行形式为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
编译器能够优化所有内容,就像使用普通递归函数一样。
成本是多少
精明的读者可能注意到了一个奇怪的细节。在从非泛型lambda到泛型lambda的过程中,我添加了一个显式返回类型(即-> int
)。为什么?
这与要推导的返回类型是条件表达式的类型有关,哪种类型取决于对self
的调用,正在推导哪种类型。快速阅读普通函数的Return类型推导会建议按照以下方式重写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示例
- 完整的代码,第一种味道
- 完整代码,第二种口味
- 完整代码,第一种风格,C++11
- 一组相互递归lambda表达式的变差
fix
的例子
它不是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,如果您想捕获函数对象外部的状态,就必须编写一个构造函数。
- C++Boost Asio Pool线程,带有lambda函数和传递引用变量
- 如何建立使用模板函数的lambda函数的尾部返回类型
- 我可以将调用类的"this"传递给 lambda 函数吗?
- 两组使用lambda函数的大括号
- 尝试将lambda函数放在队列中时出现一般分配器错误(可能是与unique_ptr有关的错误)
- 我可以在这里替换什么,因为我不能在 C# 中使用隐式变量的 lambda 函数?
- 为什么我不能在 constexpr lambda 函数中使用 std::tuple
- C++:Lambda 函数指针转换的用例是什么?
- 将 lambda 函数作为参数传递C++
- 如何将 lambda 函数作为参数发送到另一个函数
- 传递 lambda 函数的权衡是什么?
- 如何使用类模拟 C++11 中的 lambda 函数和闭包?
- 不是 lambda 函数中的常量表达式
- 如何使用可变参数数重载 lambda 函数?
- 如何通过指针传递lambda函数?
- 使用带有 lambda 函数指针的模板
- openmp c++ 中并行块内 lambda 函数的奇怪行为
- C++ 中 Lambda 函数中的溢出
- 将数组传递到 lambda 函数中
- lambda 函数未显示正确的结果