1. 自由变量(free variable)和闭包(closure)
"In computer programming, the term free variable refers to variables used in a function that are neither local variables nor parameters of that function. "
自由变量指的是相对于函数而言除了函数体内定义的局部变量和参数以外的变量.
“closure is a record storing a function together with an environment: a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or storage location to which the name was bound when the closure was created.”(Wikipedia)
简而言之,编程语言中的闭包(closure)指的是可引用自由变量的可调用实体(函数,函数指针,函数对象,Lambda表达式等).换句话说,闭包是以下两者的结合:可调用实体,其所引用的自由变量的上下文环境(referencing environment,另一种说法的"状态",state指的是同一个东西).
2. 函数对象
C++11之前不支持闭包,但函数对象可以看作是对于闭包的一种实现(或者说模拟),因为可以利用类的数据成员来保存"状态",例如:
class LessThan{ public: LessThan(int n):s_num(n){} bool operator(int num){return num<s_num;} private: int s_num; }
可以利用它为标准库的泛型算法指定参数,像这样:
vector<int> v; ... //返回v中小于2的元素个数 cout<<count_if(v.begin(),v.end(),LessThan(2)); //返回v中小于3的元素个数 cout<<count_if(v.begin(),v.end(),LessThan(3));
通过为LessThan的构造函数指定不同的参数,LessThan的对象便可保存不同"状态",调用operator()时就可具有不同的行为,这是使用函数对象相对于函数的优点之一,但定义一个类相对于函数而言增加了代码开销,下文的lambda就综合了这两者的优点.
严格来讲,由于函数对象对于状态的保存方式并不是像闭包的定义那样捕获外部自由变量,因此只能算作是闭包的一种模拟.下文所讲的Lambda底层就是基于函数对象(也就是factor)实现的,因此实际上是一种语法糖.具体可见http://www.zhihu.com/question/25740516.
3. Lambda表达式
C++对闭包的正式支持是C++11引入的Lambda表达式,它实现了"匿名函数",基本语法像这样:
捕获列表:指定lambda表达式所使用的自由变量以及其传递方式,若变量按引用传递,则需在变量名前加"&",否则默认为按值传递,单独的"="意为按值传递所有的变量,单独的"&"因为按引用传递所有的变量,以下是实例:
[a,&b] //按值捕获a,按引用捕获b [=] //按值捕获所有外层作用域的变量 [=,&a] //按值捕获除a以外所有外层作用域的变量,按引用捕获a [&,a] //按引用捕获除a以外所有外层作用域的变量,按值捕获a [] //空捕获列表,不捕获任何函数体外部定义的变量
注意:(1). 如果使用单独的"&",那么捕获列表中的其他变量不能再使用"&"("="同),此外,同一保留字在捕获列表中至多出现一次:
[&a,&a] //错误 [=,a] //错误,相当于出现了两次 [=,&a] //正确,为a指定不同的捕获方式 [&,&a] //错误 [&,a] //正确,为a指定不同的捕获方式
(2). 按值捕获的变量默认为const,因此按引用捕获的变量可以被修改而值捕获的变量不能(即使使用mutable限定符,修改的也只是副本).此外,(无论是否使用mutable)如果被捕获的变量在lambda被定义后修改,它在lambda表达式中的值还是原来的值,像这样:
int a(0); auto p = [a]()mutable{cout << a << endl; }; p(); //输出0 ++a; p(); //输出0
原因正如之前所讲,lambda底层实现借助于函数对象,具体见http://www.zhihu.com/question/25740516.
(3). lambda表达式只可以捕获带有"automatic storage duration"的变量,也就是说,它不能捕获全局变量.
参数列表:指定lambda表达式的参数,如果无参数且不包含mutable限定符和异常描述以及返回类型说明,参数列表可省略,像这样:
[...]{} //没有参数且不使用mutable限定符和异常列表
如果使用了mutable限定符或者异常描述或者返回类型说明,那么参数表不能省略(即使没有参数).
mutable限定符:因此,按值捕获的变量默认为const,如果要对其修改,需要使用在lambda的定义语句中使用mutable限定符,但即使使用了mutable,修改的也只是副本.
异常说明:同普通函数的异常说明.
返回类型说明:如果lambda表达式无返回值或仅仅包含一个普通的返回陈述,那么可省略返回类型说明,返回类型又编译器自动推导.(不普通的返回类型指返回lambda表达式,详见https://msdn.microsoft.com/en-us/library/dd293599.aspx#higherOrderLambdaExpressions)
函数体:同普通函数函数体.除此之外它可以使用捕获列表内的变量.
综上,对于2的函数对象,用lambda表达式代替,就会像这样:
int num(2); cout<<count_if(v.begin(),v.end(),[num](int n){return n<num;}));
优势很明显,减少了代码量,同时由于函数在使用的地方定义,提高了代码的可读性(对懂lambda表达式的人而言).
注:(1). Lambda表达式在用法上像函数,体现为没有捕获列表的lambda表达式可以为具有相同参数和返回类型的函数指针赋值,在实现上又基于函数对象(正如之前所言),因此有人把lambda表达式理解为匿名函数,有人理解为函数对象(个人认为理解为函数对象更加接近于本质).
(2). 既然Lambda的实现基于函数对象,因此可以推知变量的值捕获实际上是变量拷贝,从以下可以看出:
#include<iostream> using namespace std; int main() { int a, b, c; auto la = [a]{}; auto lb = [a, b]{}; auto lc = [a, b, c]{}; cout << sizeof(la) << endl //输出4 << sizeof(lb) << endl //输出8 << sizeof(lc) << endl; //输出12 system("pause"); return 0; }
(3). 每一个lambda表达式都具有不同类型,即使声明和定义完全一样的lambda表达式的类型名也不同!这是由于lambda表达式本身就是编译器生成的匿名类的对象.因此要使用lambda表达式的类型,只能利用auto和decltype(C++11新加入的关键字)做自动类型推演.
4. std::function
function类模板是C++11引入的类模板,其功能是为C++中的可调用实体(包括函数,函数对象,lambda表达式)提供一层封装,使得不同类型的可调用实体具有相同类型和接口,正如一句老话:"There is no problem in computer science that can’t be solved by adding another level of indirection.".
function类模板的实例化格式像这样:
function<返回类型(参数类型)>
例如:
#include<iostream> #include<functional> using namespace std; class LessThan{ public: LessThan(int n) :num(n){} bool operator()(int x){ return x<num; } private: int num; }; bool less_than(int num){ return num < 10; } int main(){ int m(10); function<bool(int)> f[3]={LessThan(10),less_than,[m](int num){return num<m;}};//不同实体通过封装成相同的function对象而具有相同类型,从而能放在同一数组中 system("pause"); return 0; }
注意:(1). 可调用实体要转换为function,需满足类型兼容性的条件:function的参数能转换为可调用实体的参数,可调用实体的返回值能转换为function的返回值,任何类型都能转换成void.
(2). function绑定到全局函数和类的static函数,如果要绑定到类的成员函数,则需要使用bind函数模板先做一层封装.
5. bind
bind是C++11加入的函数模板,主要用于对可调用实体进行封装而产生具有不同参数的可调用实体,起到适配器的功能,像这样:
#include<functional> #include<iostream> using namespace std; bool less_than(int num, int n){ return num <n; } int main(){ function<bool(int)> f=bind(less_than,placeholders::_1,10);//通过为less_than增加一个参数以及使用bind,也可是函数具有"保存状态"的能力(虽然底层还是函数对象) cout << f(1) << endl; system("pause"); return 0; }
使用方法:
(1). 如果要为可变参数预留位置,使用_1,_2,_3...它们是占位符,用于表明当通过封装后的函数调用封装前的函数时,参数在封装前函数中的位置(位于std的嵌套命名空间placeholders内),使用占位符不仅可以为可变参数预留位置,还可以起到翻转参数顺序的作用,如
void f(string s, char b, double a){... } function<void(double,char,string)> f1= bind(f, placeholders::_3, placeholders::_2, placeholders::_1); ... f1(1, 'a', "sas"); //f("sas",'a',1);
注意,占位符在函数内只能是顺序或逆序的,其他乱序无法通过编译!
(2). 如果要绑定重载函数,需指定重载函数的参数,像这样:
void f(double n){...} void f(char n){...} ... function<void()> f1 = bind(f,'a'); //错误,二异性 function<void()> f2 = bind((void(*)(char))f,'a'); //正确,但很丑陋
(3). bind默认按值将按值绑定参数,如果要按引用绑定参数,需要使用ref和cref(const reference)函数,像这样:
char a('a'); void f(char& n){ n = n + 1; } function<void()> f1 = bind(f,ref(a)); function<void()> f2 = bind(f,a); int main(){ cout << a << endl; //输出'a' f1(); cout << a << endl; //输出'b' f2(); cout << a << endl; //输出'b' system("pause"); return 0; }
参考自:Closure(computer programming)
有效使用 Lambda 表达式和 std::function[翻译]