C++11 中的 lambda 函数
λ \lambda λ 演算是个非常古老的计算机语言领域的技术,这段历史一直可以追溯到20世纪30年代。编程语言最先引入 lambda 函数的是 LISP 语言。之后 LISP 的各种变种中都保留了 lambda 函数的概念。
这里不准备就 lambda 函数的历史展开介绍。主要是说说 C++11 标准中的 lambda 函数。
C++ 11中的 lambda 函数可以认为就是 Functor 的一个语法糖而已。而且灵活性比 Functor 差得多。所以这里我就用 Functor 来讲解 lambda 函数。
什么是 Functor
Functor 中文一般翻译为仿函数,实际上是一个类。这个类重载了 operator () ,所以对外表现像个普通的函数。比如下面的例子:
class Add
{
public:
double operator()(double a, double b)
{
return a + b;
}
};
这个 Add 类就是个 Functor ,我们可以像个函数那样使用它。比如
int main()
{
class Add add;
double a = 1, b = 2;
double c = add(a, b);
return 0;
}
这个代码给 c 赋值的语句中的add(a, b) 看起来很像是个普通函数,但是它其实是个类的成员函数。当然在这个例子中,用个简单的函数更简便。但是如果我们的“函数”需要大量的状态变量。或者我们“函数”接口已经定了——比如这个函数会被传递给一个模板函数中,那么它的接口就必须是模板函数规定的那样,无法随意扩展,那么这时一个Functor 就体现出优势了。
这里还是举个特别的简单的例子:
class Functor
{
public:
Functor(int op) : op(op) {}
double operator()(double a, double b)
{
if(op)
return a + b;
else
return a - b;
}
private:
int op;
};
这个例子中 op 就是“函数” 的状态。通过状态可以影响函数的行为。
那么使用它时可以这样:
int main()
{
Functor add(1), sub(0);
double a = 1, b = 2;
double c = add(a, b);
double d = sub(a, b);
}
这里 add 和 sub 都是 Functor 的具体实例,一个实现加法,另一个实现减法。具体实现的是哪个功能,由初始化对象时确定。
λ \lambda λ 函数
Functor 很灵活,但是也挺麻烦。有时我们需要的这个 Functor 其实只用一次。为这种只用一次的地方专门写一个类有点叠床架屋的感觉。 lambda 函数可以认为就是 Functor 的一种简写方式。可以让我们的代码更短。当然有利也有弊,lambda 函数只能代替一些简单的 Functor。过于复杂的 Functor 用 lambda 函数实现出来是有困难的。
在新版的 C++ 标准中(C++14、C++17 等),lambda 函数的功能有增强。但是本文还是只限于 C++11。
lambda 函数的语法定义如下:
[capture](parameter)mutable -> return-type{state}
- capture 是捕捉列表。
- parameter 是参数列表,就是函数的那些传入变量。
- mutable 这个后面再介绍。
- return-type 返回值的类型,如果返回值明确,也可以省略。
- state 是函数体
我们还是用例子来说明,比如最初的那个 Add 函数。可以写为:
auto add = [](double x, double y) -> double {return x + y;};
我们的代码可以这样使用:add(1, 2)。这个代码的返回值类型其实是很明确的,所有可以简写为:
auto add = [](double x, double y) {return x + y;};
当然并不是什么时候都可以省略返回值类型的。比如说下面这样:
auto add = [](int x, int y) ->double {return x + y;};
上面这个代码中的 double 就不能省略,因为直接推导出的类型是 int。而我们却需要 double 类型的返回值。
下面来讲解捕捉列表。通过捕捉列表,我们可以获得 context 中的一些数据。如果没有捕捉列表我们就只能通过参数传递的方式把这些参数传进来。这里所谓的 context (上下文) 指的就是 lambda 函数所在的作用域。比如说 lambda 函数是定义在一个普通函数中的,那么 context 指的就是这个这个函数作用域。也就是说可以通过捕捉列表获得所在的这个函数局部变量的信息。如果 lambda 函数是定义在一个类的成员函数中的,那么除了成员函数局部变量,还可以获得 this 指针,间接地也就获得了这个类的成员变量的数据。
捕捉列表由一个或多个捕捉项组成,并以逗号分隔,捕捉列表一般有以下几种形式:
- [var] 表示值传递方式捕捉变量 var
- [=] 表示值传递方式捕捉所有父作用域的变量(包括this指针)
- [&var] 表示引用传递捕捉变量 var
- [&] 表示引用传递捕捉所有父作用域的变量(包括this指针)
下面举几个例子:
- [=,&a,&b] 表示以引用传递的方式捕捉变量 a 和 b,而以值传递方式捕捉其他所有的变量。
- [&,a,this] 表示以值传递的方式捕捉 a 和 this,而以引用传递方式捕捉其他所有变量。
- [=] 表示值传递方式捕捉所有变量。
下面举个简单的例子:
int main(int argc, char *argv[])
{
double aa = 1, bb = 2;
int cc = 0;
auto func = [cc](double x, double y) ->double
{
if(cc)
{
return x + y;
}else
{
return x - y;
}
};
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
cc = 1;
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
return 1;
}
输出的结果如下:
func(1, 2) = -1
func(1, 2) = -1
从上面的例子可以看出,cc 并不是作为参数传进去的,而是捕捉进去的。那它是什么时候捕捉进去的呢,是在函数定义时就捕捉的。后面调用时不会再次捕捉,所以第二次调用时虽然 cc 已经变成 1 了。但是在 lambda 函数中 cc 还是 0。
我们可以用 Functor 翻译这个 lambda 函数:
class Functor
{
public:
Functor(int cc): cc(cc){}
double operator() const (double x, double y)
{
if(cc)
{
return x + y;
}
else
{
return x - y;
}
}
private:
int cc;
};
int main(int argc, char *argv[])
{
double aa = 1, bb = 2;
int cc = 0;
Functor func(cc);
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
cc = 1;
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
}
这个代码可上面的 lambda 函数实现是等价的。实际上现在的 C++ 编译器就是把 lambda 函数翻译成 Functor 。这里有个需要注意的地方:
double operator() const (double x, double y)
这一行中有个 const。 说明 operator() 被声明为 const 函数了。所谓 const 函数就是它不能改变类的状态。如果我们需要它不是 const 函数就要用到 mutable 关键字了。
如果我们需要能够感知 cc 的变化,可以用引用捕捉的方式。比如下这样:
int main(int argc, char *argv[])
{
double aa = 1, bb = 2;
int cc = 0;
auto func = [&cc](double x, double y) ->double
{
if(cc)
{
return x + y;
}else
{
return x - y;
}
};
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
cc = 1;
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
return 1;
}
这个代码的结果:
func(1, 2) = -1
func(1, 2) = 3
翻译成 Fucotor 是下面这样的:
class Functor
{
public:
Functor(int &cc): cc(cc){}
double operator() const(double x, double y)
{
if(cc)
{
return x + y;
}
else
{
return x - y;
}
}
private:
int &cc;
};
int main(int argc, char *argv[])
{
double aa = 1, bb = 2;
int cc = 0;
Functor func(cc);
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
cc = 1;
std::cout << "func(1, 2) = " << func(aa, bb) << std::endl;
}
因为 cc 是引用类型,所以即使 operator() 是 const 函数我们也是可以改变 cc 的值的。
关于 lambda 函数,知道这些基本也就够了。