C++11之Lambda表达式

时间:2021-07-31 18:42:50

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。