python的闭包
闭包这个概念一直有所耳闻,在以前看《Java编程思想》时第一次真正接触,当时的理解就是类似C++11lambda表达式捕获外部变量的东西,只不过Java5语法相对简陋,还得用匿名内部类的方式来实现。今天看《Python核心编程 第二版》才恍然大悟,闭包的意义。
You do not want to have f2()'s stack frame around because that will keep all of f2()'s variables alive even if we are only interested in the free variables used by f3 (). Cells hold on to the free variables so that the rest of f2() can be deallocated.
相应代码如下
def f2():
x = 1
y = 2
z = 3
def f3():
y = 3 # 局部变量y覆盖了外部变量y
print(x, y) # 引用了外部变量x, 此时x即闭包变量
return f3
函数f2()返回一个内部函数,之后用于可以像这样调用内部函数f3()
f = f2()
f()
而由于f3()被f2()包裹起来了,用户无法通过f2()以外的方式来取得f3()
f3() # 抛出NameError异常
闭包的高明之处在于,正如文章最初引用的原文所述,f3()需要引用f2()栈帧的变量,正常来说f2()在调用完毕后栈帧会销毁,由于变量x被f3()引用了,x不会被销毁。
PS:说明下栈帧的概念,一个程序会占用一段虚拟内存空间。除了某些静态字段(比如常量/代码段)外分为2个区域,栈和堆,栈存放静态分配的内存,自动增长和回收。比如声明一个变量,程序栈的部分就会增长来存放这个变量,变量销毁后,栈的这部分被自动回收。而像C++中动态malloc/new分配一段内存,则会增长堆的部分,这部分得程序手动回收。在python等相对更高级的语言中会用虚拟机/解释器的垃圾回收功能来回收。那函数的栈帧呢?函数和类一样,都可以看作高级对象嘛。
C++的闭包
实现方式
C++的函数对象是用类和operator ()来实现的,让对象的行为表现得像函数一样。
struct F2 {
int x = 1;
int y = 2;
int z = 3;
struct F3 {
int x;
int y = 3;
F3(int x_) : x(x_) {}
void operator()() const {
printf("%d %d\n", x, y);
}
};
F3 operator()() const {
return F3(x);
}
};
调用方式如下
auto f = F2()();
f();
C++11的lambda表达式使上述代码得到了简化
#include <functional>
struct F2 {
int x = 1;
int y = 2;
int z = 3;
std::function<void (void)> operator()() const {
auto& x = this->x;
return [x]() { int y = 3; printf("%d %d\n", x, y); };
}
};
这里注意一点,auto& x = this->x
这句是必要的,因为访问类成员变量的方式实际上是this->x
,只能捕获this而不能捕获类的内部成员,除非像这样较为蹩脚的做法,定义一个局部引用指向类成员变量,再捕获引用对象的拷贝。
在C++14中可以用[x = x]的捕获方式,gcc编译选项为-std=c++14
上述内容参考这篇文章lambda表达式
C++的问题
其实从上述代码就可以看出来了,这里是捕获一份拷贝,而非引用。如果成员变量x是个体积较为庞大的对象呢?比如std::vector<int>
或std::array<int, 1000>
之类的?拷贝的开销还是很大的,那么拷贝引用如何?
std::function<void (void)> operator()() const {
auto& x = this->x;
return [&x]() { int y = 3; printf("%d %d\n", x, y); };
}
调用方式如下
auto f = F2()();
f();
在这里的测试代码是没问题的,大概是编译器优化,临时对象还存活?但如果像下面这样
std::function<void (void)> foo()
{
F2 f2;
return f2();
}
int main() {
auto f = foo();
f();
return 0;
}
打印结果如下
xyz@ubuntu:~/python-learning/function/closure$ g++ test.cc -std=c++11
xyz@ubuntu:~/python-learning/function/closure$ ./a.out
-1558910688 3
因为lambda表达式捕获的引用对象已经随着类的对象销毁而销毁了,这和C的悬挂指针如出一撤,也是C++er常犯的问题。比如下面这样的代码
const char* foo()
{
char buf[100];
// ...
return buf;
}
上述代码的问题是,函数foo()调用完毕时,栈帧销毁,返回值(char[100]强制转换成的const char*类型)指向的数组buf是foo的栈帧的一部分,被重新回收,因此返回值指向的是被回收的对象。
C++做不到像python那样对象销毁时只销毁其中一部分变量,而那些被捕获的变量则保留。
大致而言有两种解决方式
- 把被捕获的变量声明为static类型,这样便不会随着对象销毁而销毁。缺点是static变量和全局变量一样,伴随着程序整个生存周期,会占用不必要的内存。而且在多线程环境下,多个函数对象访问static变量会产生race condition。
- 使用std::shared_ptr<T>来存储size较大的T对象,相当于模拟GC的引用计数功能。不过shared_ptr仍然存在线程安全问题,依旧得上锁访问T对象。
#include <stdio.h>
#include <vector>
#include <functional>
#include <memory>
#include <numeric>
struct Func {
std::shared_ptr<std::vector<int>> pVec;
explicit Func(size_t num) {
pVec = std::make_shared<std::vector<int>>(num);
std::iota(pVec->begin(), pVec->end(), 0);
}
std::function<void (void)> operator()() {
auto pVec = this->pVec;
return [pVec]() { for (int i : *pVec) printf("%d ", i); };
}
};
std::function<void (void)> foo(size_t num) {
Func func{ num };
return func();
}
int main() {
auto f = foo(25);
f();
return 0;
}