HelloKenLee

C++中的闭包原理分析,比如...返回一个lamda函数?

Posted on By KenLee

前言

在lua或者python等类似的动态语言中,是有Closure(闭包或闭合函数,以下统称为闭包)这个概念的。 什么是闭包呢? 就是在一个函数A里面定义另外一个子函数B,子函数B能够访问父函数A中的局部变量这样一种特性。比如在lua中:

function A()
	i = 'i love u'
	function B()
		print(i)
	end
	B()
end
-- main:
A() --> stdout: "i love u"

在函数B()看来, i即不是B()中的局部变量,也不是全局变量,却能够在B()中被访问到,叫做闭包的特性。这种特性在当i本该失效的时候更加有趣,考虑一下lua代码:

function newCounter()
	i = 0
	function counter()
		i = i + 1
		return i
	end
	return counter --> 返回一个函数
end
-- main
a = newCounter()
print(a()) --> stdout: 1
print(a()) --> stdout: 2
b = newCounter();
print(b()) --> stdout: 1

在上面的代码中,变量i在执行i = i + 1这一语句的时候已经跳出了newCouter()这一函数,一般来说i已经被销毁。但是从例子可以看出,i不但没有被销毁,还能成功的被更改。这时我们称变量i为闭包函数a()的一个“非局部变量”。同理,闭包函数b()也有非局部变量i,而且他们是相互独立的。

C++中的闭包

那么在C++中有没有闭包存在呢?如果有,闭包里面的“非局部变量”它既不是局部变量,因此不被储存在栈区;也不是全局变量或者静态变量,因此不存在静态区;更不是被动态创建出来的,因此不会在堆区。那么这些“非局部变量”存在哪呢?(太长不看版: 有;存在栈区。)

在传统C++的语法中,我们是不能够在一个函数中定义另一个函数的,因此也就不需要考虑闭包问题了。然而C++11对lambda表达式的支持让这个问题值得讨论:

C++11的lambda表达式允许我们在一个函数里面定义一个匿名的内联函数(准确来说,是允许定义一个可调用代码单元),《C++ Primer》中对lamba表达式的定义如下:

与任何函数类似,一个lambda表达式具有一个返回类型,一个参数列表和一个函数体。一个lambda表达式具有以下形式:

[capture list] (parameter list) -> return type {function body}

特别说明一下[capture list]即我们在父函数中的局部变量并且需要在子函数(lambda)中用到的变量。

因此我们不难仿照上面的lua代码写出对应的C++代码:

///!这段代码不能通过编译
typedef int (*funcPtr)();//定义一个参数为空,返回值为int的函数指针类型
//类似定义一个函数生成器,返回值是一个函数指针
funcPtr create(){
    int a = 0;// 初始化“非局部变量”
    func res = [&a](){return a++;};//引用捕获变量a,才能改变a
    return res;//返回一个函数指针
}

如果你编译上面的代码,会报error: cannot convert ‘create()::__lambda0’ to ‘funcPtr {aka int (*)()}’ in initialization|错误。原因是因为一个lambda表达式只有当它捕获列表为空的时候才能转换成函数指针。 出自《C++11 Standard》5.1.2节:

The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.

那么是不是表面C++中不能使用闭包这种特性呢? 并不是,我们可以使用std::function<T>来使用闭包,代码更改为:

#include <functional>
using namespace std;
// 函数生成器,返回一个函数对象
function<int()> create(){
    int i = 0;// 初始化“非局部变量”
	//			lambda: [捕获 i] (空参数) -> 返回int
    function<int()> res = [i]()mutable -> int{
        return i++;
    };
    return res;
}
int main(){
    auto counter1 = create();
    cout<<counter1()<<endl;// -->stdout: 0
    cout<<counter1()<<endl;// -->stdout: 1

    auto counter2 = create();
    cout<<counter2()<<endl;// -->stdout: 1
    return 0;
}

这样子我们相当于定义了一个“函数工厂”,这个工厂可以根据参数,“生产出”不同的函数。比如上面的create()函数我们可以传递进去一个值来初始化i,这样我们就可以用它来生产从不同数字开始数起的计数函数了!(示例代码看最后)

原理解析

那么为什么一个带有捕获的lambda不能转换成函数指针,却能转换成一个function<T>呢? 答案显而易见,因为函数指针只能指向一个函数,function<T>变量表示的却是一个类的实例!(准确来说是一个重载了括号运算符的类的实例,或者说,一个仿函数对象。)根据C++官方给出的解释,lambda表达式只是一个语法糖而已,事实上lambda表达式构造的就是一个function<T>对象。而捕获列表[]中的也就是这个对象的构造函数的参数而已!

换而言之,create()函数的作用就是不停地调用function<int()>这个类的构造函数,并把构造出来的对象(可调用对象)返回。

这样看来,main()函数中的counter1counter2都是function<int()>类的不同实例。也就解释了i这种所谓“非局部变量”储存在哪的问题。i是在create()中通过构造函数传进去的,因此也就是对象counter1counter2的成员变量而已。因为counetr1counter2是局部变量,i自然也就是main()中的局部变量咯!

最后附上终极版例子说明这种特性的用处:

function<int()> createCounter(int start){
    int i = start;
    return [i]()mutable -> int{return ++i;};
}
int main(){
    auto counter1 = createCounter(0);  // 从0开始异世界生活
    auto counter2 = createCounter(10); // 从10开始的计数器
    auto counter3 = createCounter(-10); // 从-10开始的计数器
    cout<<counter1()<<endl;// -> 1
    cout<<counter2()<<endl;// -> 11
    cout<<counter3()<<endl;// -> -9
    return 0;
}

参考资料

  1. https://stackoverflow.com/questions/13358672/how-to-convert-a-lambda-to-an-stdfunction-using-templates
  2. http://shaharmike.com/cpp/lambdas-and-functions/#mutable-lambdas:20d83d12ec0a04fbb356ddfee1c565a0
  3. http://www.cplusplus.com/reference/functional/function/?kw=function