闭包
假设有这样的需求:计算 ,但 是一个不固定的数,需要用户输入。按照之前的想法,我们可以向 sum
传入一个 Lambda 表达式:
#include <iostream>
#include <cmath>
double sum(int a, int b, double term(int)) {
return a > b ? 0.0 : term(a) + sum(a + 1, b, term);
}
int main() {
int n;
std::cin >> n;
// 定义于 <cmath> 的 std::pow 可计算 a^n 的值
sum(1, 5, [](int a) { return std::pow(a, n); });
}
但这里就引入了上一节遗留的问题:Lambda 表达式中不能使用局部变量 n
。当然你可以将 n
设置为全局变量;但这里我如果就不这样做,非要是局部变量,那该如何处理?
答案是使用闭包(Closure)。闭包是一个很特别的编程术语,专门指代可以读取其它函数局部变量的函数。我们要在这一节手动实现一个闭包,方法是:定义一个特别的类,以及它的 operator()
。
首先,定义一个类 MyLambda
,以及其中的一个成员数据 n
:
这样,我们可以初始化一个 MyLambda
类的对象以及它的成员为用户输入的值:
随后,为 MyLambda
类定义 operator()
。这样,lambda
就可以像一个函数一样使用:
#include <iostream>
#include <cmath>
class MyLambda {
public:
int n;
double operator()(int a) const {
return std::pow(a, n);
}
};
double sum(int a, int b, double (*term)(int)) {
return a > b ? 0.0 : term(a) + sum(a + 1, b, term);
}
int main() {
int n;
std::cin >> n;
MyLambda x{n};
sum(1, 5, x); // 像函数一样使用对象 x……
}
但可惜的是,这样编译并过不了。尽管对象 x
可以像函数一样使用,比如 x(a)
,但它并不能转换到 double(*)(int)
类型。只要解除这个类型限制,就可以实现所有的功能了。解除限制的方法很简单,就是让这个 term
形参的类型变成一个模板形参:
template<typename F>
double sum(int a, int b, F term) {
return a > b ? 0.0 : term(a) + sum(a + 1, b, term);
}
将 sum
改成这个样子之后,就无关乎 term
的具体类型是什么了:如果传入的实参是 double(*)(int)
,那 term
就是函数指针;如果传入的实参是刚刚的 x
,那 term
就是 MyLambda
类型。随后,只要 term(a)
是合法表达式,那编译就没有问题了。以下是完整代码:
#include <iostream>
#include <cmath>
class MyLambda {
public:
int n;
double operator()(int a) const {
return std::pow(a, n);
}
};
template<typename F>
double sum(int a, int b, F term) {
return a > b ? 0.0 : term(a) + sum(a + 1, b, term);
}
int main() {
int n;
std::cin >> n;
MyLambda x{n};
std::cout << sum(1, 5, x) << std::endl;
}
类似这份代码中的 term
,可以以 term(a)
的形式调用,或者说出现在函数调用运算符的左侧的对象,称为可调用对象(Callable object)。常见的可调用对象有:
- 函数;
- 函数指针;
- 可转换到函数指针的对象;
- 重载了
operator()
的对象(即“函数对象”);Lambda 表达式。
带捕获的 Lambda 表达式
事实上,Lambda 表达式的完整版其实就是我们刚刚实现的闭包。刚刚的代码和下面的写法是一致的:
注意其中的 [n](int a) { return std::pow(a, n); }
。这是一个 Lambda 表达式,但与之前不同的是,它开头的中括号内给出了额外的 n
字样。这个中括号是 Lambda 的捕获(Capture)语法。如果你想要在 Lambda 表达式中使用一些局部变量,则需要将它的名字放在捕获里。比如这个 Lambda 表达式中使用了局部变量 n
,所以我要把 n
放在捕获的中括号里。Lambda 表达式实际上就是一个匿名类的对象,类似我们之前的 MyLambda
的实现。它提供了 operator()
以像函数一样使用这个对象。如果存在捕获,则将这些捕获定义为类的成员,并用局部变量的值初始化它们。
Lambda 表达式具体的捕获语法比较复杂,我将这些细节问题放到了下一节。