2018-11-04 | 研究与探索 | UNLOCK

谈谈闭包

闭包(closure)是函数式编程中的概念,指新生成的函数对象离开初始作用域后仍然“携带”原来作用域里的变量。 用一个比较经典的例子,是阮一峰老师翻译的《黑客与画家》提到过的(阮一峰老师的博客上写到过):设计一个工厂函数,它接受一个值n,返回一个累加器,这个累加器以n为初始值,每次接受一个值i,将存储的值加上i后返回。

作者为了说明 Lisp 语言的先进性列举了很多语言中对此的实现,我觉得其中 JavaScript 的代码最好懂:

1
2
3
function foo (n) {
return function (i) {
return n += i } }

对作用域很敏感的人可能要问了:foo返回的匿名函数的函数体中使用的n在出foo后就失效了啊,怎么还能加呢?其实这个nfoo中最外层n的一个副本,所属权是那个匿名函数,它的内存由GC负责释放。更棒的是,每次生成的匿名函数持有的n是不同的变量,互不干扰。

这样可以看出“闭包”这个名字很形象,函数“包”着上一个作用域中的变量到其它地方去。 在那篇文章里又说

其他语言怎么样?前文曾经提到过Fortran、C、C++、Java和Visual Basic,看上去使用它们,根本无法解决这个问题。

这么评价C++是不公正的,C++11前虽然没有函数字面量,但是你可以定义一个带operator()方法的类,然后类可以有成员,这样就能实现要求的效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
class Counter
{
public:
Counter(T _init):m_init(_init){}
T operator()(T cnt)
{
return m_init+=cnt;
}
private:
T m_init;
};

template <typename T>
T foo(T n)
{
return Counter(n);
}

美中不足的是C++11前虽然能在函数中定义类,但是函数的返回类型要提前声明,所以只能在函数外定义Counter类。

C++11给我们带来了lambda函数和function模板,代码就可以变成这样:

1
2
3
4
5
6
#include <functional>
template <typename T>
std::function<T(T)> foo(T n)
{
return [n](T i){return n+=i;};
}

注意我们声明了在返回的lambda中n是按值绑定的。但这段代码其实不能通过编译……因为C++中lambda函数的按值绑定默认是不可变的。 当然我们还可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <functional>

template <typename T>
std::function<T(T)> foo(T n)
{
class Counter
{
public:
Counter(T _init):m_init(_init){}
T operator()(T cnt)
{
return m_init+=cnt;
}
private:
T m_init;
};
return Counter(n);
}

这样不会给foo所在命名空间中增加一个类,也能满足泛型,但这样Paul Graham先生就会笑话我们写的不够简洁,还要手动添加闭包涉及的变量…… 幸好C++11是有解决办法的,能成功编译运行的代码如下:

1
2
3
4
5
6
7
#include <functional>

template <typename T>
std::function<T(T)> foo(T n)
{
return [n](T i)mutable{return n+=i;};
}

这个mutable关键字恐怕是用的最少的C++关键字,有兴趣的可以查查它的用处。在这里它就是用于让按值绑定的变量在lambda函数体中可变。其实lambda的实现就是编译器帮你生成一个含相应成员的匿名类,再生成它的一个对象,所以不用担心没有GC的C++怎么回收lambda闭包里携带的变量。

主要归功于C++11,可以说让C++变得焕然一新。