问题的关键在于,在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)...); }
};
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(); // this has no impact
}
事实上,GCC只接受第一种形式的fix_type
代码(传递了functor
的那种)。我无法确定是否应该抱怨另一种形式(其中传递了*this
)。我让读者选择权衡:更少的类型推断还是更少的丑陋递归调用(当然也完全可以访问任何一种风格)。
GCC 4.9示例