问题的症结在于,在 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 < 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),应该是这样。
最后,这是为使用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 的转变中,我添加了一个显式返回类型(即-> 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 示例