原文:zhihu
首先,什么是lambda
?
是闭包,是函数对象(重载了operator()
的类型),是一种持有状态的函数
当写下
auto a = 2;
auto f = [&](int x) {return a * x;};
时,编译器会把代码翻译成大概下面这样,生成一个匿名类(https://cppinsights.io/翻译的结果)
int a = 2;
class __lambda_6_12
{
public:
inline /*constexpr */ int operator()(int x) const
{
return a * x;
}
private:
int & a;
public:
__lambda_6_12(int & _a)
: a{_a}
{}
};
__lambda_6_12 f = __lambda_6_12{a};
lambda
的优点在于简化了函数对象的编码,把函数对象定义在了调用处,同时减少了命名(无需类型命名,只需命名变量或无需命名),如果不打算重用函数,比如std::thread
构造函数传入的线程运行函数,可以考虑lambda
但如果lambda
一定需要递归实现呢?
比如说下文中我们想实现一个递归打印数字的count_down
函数,其中分隔符sep
不是固定的
对于常规函数与函数对象(闭包),可以维护一个全局变量或成员变量sep
std::string sep = get_sep();
auto count_down(int x) -> std::string
return x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1);
}
或者把sep
作为一个额外形参
auto count_down(int x, std::string const& sep) -> std::string {
return x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1, sep);
}
而对于lambda
,我们可以首先尝试一下把lambda
式变量名按引用捕获到lambda
函数体里面
std::string sep = get_sep();
auto count_down = [&](int x) -> std::string {
return x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1);
};
oops
.. 编译失败了,报错是
error: variable 'count_down' declared with deduced type 'auto' cannot appear in its own initializer
因为我们需要把右侧匿名的lambda
赋值给左侧auto
的变量count_down
,而对于编译器,需要解析完赋值表达式的右侧才能确定auto
对应的lambda
类型,所以我们在函数体里使用类型未定的count_down
自身时,很不幸,编译器拒绝了类型推导
所以问题在于在递归调用时需要确定lambda
本身的类型
第一种解决方案,使用std::function
std::string sep{", "}
std::function<std::string(int)> count_down = [&](int x) -> std::string {
return x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1);
};
cout << count_down(5);
//5, 4, 3, 2, 1, 0
可以编译了的原因是std::function
对象内部存储了lambda
可调用对象,std::function
调用operator()
时,会把实参完美转发到内部的可调用对象,由于指定了std::function
的模板参数类型,我们回避了匿名类型的问题
然而这里有两个小问题
- 由于
lambda
存在function
里,递归的调用链是function->lambda->function->lambda->...
- 函数签名重复写了两次,太丑了
好在C++14
支持了泛型lambda
,回想下问题在于在lambda
递归调用时需要确定本身的类型,那我们其实只给lambda
多加一个指向自身的泛型形参就好了,实现如下
auto count_down = [impl = [&](auto const &self, int x) -> std::string {
return x == 0 ? "0" : std::to_string(x) + sep + self(self, x - 1);
}](auto &&...args) {
return impl(impl, std::forward<decltype(args)>(args)...);
};
思路就是把lambda
式存在另一个闭包里,闭包调用时,调用实际的递归函数,同时把它自身的引用作为第一个参数,这样实际上lambda
对象只是不断递归调用自身
当然这么写还是麻烦,我们把生成新闭包和转发实参的部分也写成一个如下的泛型lambda
(函数装饰器)
auto recursive_lambda = [](auto &&lam) {
return [lam_impl = std::forward<decltype(lam)>(lam)](auto &&...args) {
return lam_impl(lam_impl, std::forward<decltype(args)>(args)...);
};
};
这样就实现了递归lambda
式
std::string sep{"..."};
auto count_down = recursive_lambda([&](auto const &self, int x) -> std::string {
return x == 0 ? "0" : std::to_string(x) + sep + self(self, x - 1);
});
cout << count_down(5);
//10...9...8...7...6...5...4...3...2...1...0
注意上述recursive_lambda
使用时,如果返回类型不是void
,需要在lambda
形参之后,函数体之前使用->
尾置返回类型