C++11提供了对匿名函数的支持,即 lambda 函数(表达式)。C++11的 lambda 表达式语法格式如下:
//[capture](parameters)->returnType{body} //[关联](参数)->返回类型{表达式内容}
如果没有参数,这里的小括号就可以省略。如果表达式里只有一个return语句,或者是返回void,则返回值也可以省略掉。
//[capture](parameters){body}
参数、返回值、表达式体
我们先了解一下lambda表达式的参数、返回值、表达式体。
下面就C++的lambda表达式举内处例子:
[](int x, int y) { return x + y; } // 只有一个return语句,返回类型可省略。 [](int& x) { ++x; } // 函数体里没有return语句。即此匿名函数返回void类型。 []() { ++global_x; } // 没有参数,表达式里直接使用全局的变量。 []{ ++global_x; } // 同上,因为没有参数,所以小括号可以省略。 [](int a, int b)->int{ double c = sqrt(a * b); return round(c);} // 带有返回值声明的lambda表达式。
其实,如果没有指定返回值,lambda表达式的返回值是通过C++的decltype(x+y)确定的(对于上面第一个例子)。事实上,如果lambda表达式内的所有return语句的decltype(XXX)都是一样的,或者没有return语句时,都可以省略其返回类型。
关联
下面讨论一下关联参数。在其它语言或者概念里,这里的参数和表达式被称为闭包(Closure)。
C++的lambda表达式比普通的C++函数多一样东西就是它的关联参数。lambda表达式所在的定义的地方能访问的变量,如果在表达式内要使用,都可以通过中括号,把外部的变量引进过来。很多人第一次看到这东西也会搞不明白是干什么的。在很多其它支持匿名函数(或者匿名类)的语言中,并没有这个。原因是其它语言基本上都会把外部的变量自动全都引入,用C++的术语说,就是默认全部通过C++引用传给lambda表达式。在这些语言里如果想要在lambda里“修改”外部变量的值,而外部变量的值不被修改,则只能通过给lambda传参数来实现。
例如一个javascript匿名函数,可以*的使用“外部”变量:
function max(v) { var x = 0; var f = function(val) { // 匿名函数 if (v[val] > x) { // 在匿名函数里可以引用外部变量v和x。 x = v[val]; // 在匿名函数里,对外部变量的引用都是“直接”的,相当于C++中的传引用关联。可以修改外部变量的值。 } } for (k in v) { f(k); } return x; } alert(max([1, 6, 3, 0, 2])); // 提示结果为“6”
就像本节开始所说的,在lambda表达式内中括号关联引用外部变量,被叫做闭包。在C++中,闭包可以通过传值关联,也可以通过传引用关联。这里先科普一下“闭包”。在数学里,闭包就是对一个集合执行某运算,如果运算结果还是这个集合,则这个集合就是这个运算的闭包。在程序中,闭包的概念更广义一些,就是lambda函数执行所需要的所有运行时环境、还有函数本身。运行时环境就包括函数体外部定义的非全局的,可以被lambda直接使用的被关联的变量。
举几个例子:
[] // 没有任何关联,如果在lambda里引用外部参数。则会报编译错误。 [x, &y] // 外部变量x是通过传值关联,y是通过传引用关联。 [&] // 默认传引用关联,在lambda里所有使用外部的变量都使用外部变量的引用。 [=] // 默认传值关联,在lambda里所有使用外部的变量都是传值的。 [&, x] // 变量x为传值关联,其它外部变量则使用传引用关联。 [=, &z] // 变量z为传引用关联,其它外部变量则使用传值关联。
用C++代码举个例子:
std::vector<int> some_list{ 1, 2, 3, 4, 5 }; int total = 0; std::for_each(begin(some_list), end(some_list), [&total](int x) { total += x; });这个例子会计算some_list的内容的和。由于是传引用关联的total,所以lambda里对total的修改会让外部的total也看到变化。
std::vector<int> some_list{ 1, 2, 3, 4, 5 }; int total = 0; int value = 5; std::for_each(begin(some_list), end(some_list), [&, value, this](int x) { total += x * value * this->some_func(); });
这段代码里value中传值关联的。total没有指定,但是有默认使用引用关联,所以total会以引用关联。
这里还关联的一个特殊变量this。在C++里this只能通过传值关联,不能传this的引用。当然另外就是传this时只能是在类的非静态成员函数中使用。在lambda里使用this时,具有和lambda所在的函数对this同样的访问权限,即可访问本对象的private变量,可访问父对象的protected变量,等等。
在lambda里关联this后,也和lambda所在的函数里一样,在访问this成员时,在没有歧义时,可以省略“this->”。
高级主题
实现了C++11的lambda表达式的编译器,在内部实现细节上,可以不一样,但是,因为lambda一般都是作用域小、没有内部局部变量、函数体简单的,所以一般情况下,C++中的lambda都是可以通过inline优化,去掉多余的栈空间使用,从而把lambda表达式优化到像使用C的宏函数一样。
使用lambda时,经常为了处理异步调用,而让lambda表达式所在的函数走完,才去再调用这个lambda。在javascript中这很常见,特别是现在的node.js,由于javascript中的函数也是对象,所以在lambda调用时,lambda里所引用的外部变量还是有效的。在C++规范中,这样的行为是未定义的。因此,在C++中这样做,可能会让程序直接异常退出。
到目前为止,我们只讲了实现C++的lambda,在C++中,一个函数也可作为一个变量类型。那么lambda是什么类型的呢?
在各个编译器中,lambda的具体实现类型可能都不一样。在接收lambda为参数时,或者需要直接把lambda传给一个变量时,则可以使用std::function或者类似的类来接收lambda。因为C++11引入了简化的自动变量,所以我们也可以在这儿用上auto关键字:
auto my_lambda_func = [&](int x) { /*...*/ }; auto my_onheap_lambda_func = new auto([=](int x) { /*...*/ });这里再举一个例子,是把lambda保存在变量里,vector里和数组里,然后作为参数传给别的函数:
#include<functional> #include<vector> #include<iostream> double eval(std::function<double(double)> f, double x = 2.0){return f(x);} int main(){ std::function<double(double)> f0 = [](double x){return 1;}; auto f1 = [](double x){return x;}; decltype(f0) fa[3] = {f0,f1,[](double x){return x*x;}}; std::vector<decltype(f0)> fv = {f0,f1}; fv.push_back ([](double x){return x*x;}); for(int i=0;i<fv.size();i++) std::cout << fv[i](2.0) << "\n"; for(int i=0;i<3;i++) std::cout << fa[i](2.0) << "\n"; for(auto &f : fv) std::cout << f(2.0) << "\n"; for(auto &f : fa) std::cout << f(2.0) << "\n"; std::cout << eval(f0) << "\n"; std::cout << eval(f1) << "\n"; std::cout << eval([](double x){return x*x;}) << "\n"; return 0; }
在C++里,一个没有关联参数(即中括号为空)的lambda表达式,是可以隐式地转成同类型的函数指针的。所以这样的调用是合法的:
auto a_lambda_func = [](int x) { /*...*/ }; void(*func_ptr)(int) = a_lambda_func; func_ptr(4); //调用lambda函数。附注:本文主要取材自Wikipedia。