Lambda表达式

C++11引入了Lambda表达式,也称作匿名函数,本文讨论下lambda表达式的用法和原理。

什么是Lambda表达式

先看一个最简单的lambda表达式:

1
2
auto f = [](){ std::cout << "hello lambda\n"; };
f();

以上代码定义了一个lambda表达式f,它是一个可调用对象,可简单理解为一个函数,没有入参,没有返回值,唯一的功能就是打印 hello lambda,紧接着就像调用函数一样调用它,打印信息。

我们看到,定义一个lambda表达式就是定义一个可调用对象,就像仿函数或者函数指针一样,但是它的类型是不确定的,通常就用auto来表示,而且一般也不需要明确知道它的类型,当然,如果要明确写出,也可以像函数指针的类型或者std::function的定义类型那样写出来。

那么,lambda表达式有什么用呢,对写代码有什么提升呢?假设这样的场景,我们写一段逻辑时,临时需要写一个函数,这个函数功能很简单,就两三行,而且就只在当前环境使用。以前的做法是定义一个函数,然后在当前环境调用它。那有了lambda表达式,就不需要这样做了,因为lambda表达式自身就是一个表达式,我们可以在代码的任意地方像定义一个整型变量一样定义它,所以可以直接在需要使用的地方把定义出这个匿名函数。这可能在我们平常的代码中优势不是很明显,但是在标准库的使用上却提供了极大的便利性。因为标准库大量使用了仿函数,比如标准库的排序算法,如果我们要排序的对象没有默认的比较大小的实现,就需要调用者自己实现一个比较大小的仿函数。有了lambda表达式,我们就可以将lambda表达式作为参数传递给标准库的算法。

基本语法

lambda表达式的完整形式如下:

[capture list] (params list) mutable exception -> return type { function body }

其中:

parmas list 是函数的形参列表;

exception 是函数抛出的异常;

return type 是函数返回值;

function body 是函数的具体内容;

capture list 和 mutable 后面的内容再描述。

其中 mutable exception 和 return type都是可以省略的,当者三者都省略时,params list 也可以省略。

原理分析

这里说原理分析可能也不太准确,因为我也没有看过具体的实现,只能给出它的等效实现。

假如我们有如下代码:

1
2
3
int x = 5;
auto f = [x](int y) mutable -> bool { bool result = x == y; cout << x++; return result; };
bool result = f(4);

以上代码没什么实际意义,存粹突然瞎写的。

这里lambda表达式中的[] 中的x表示捕获,捕获有值捕获和引用捕获两种形式,如果捕获的内容写为&x就是按引用捕获,没有&就是按值捕获。mutable就是针对值捕获的,如果写了这个关键字,就表示值捕获的内容在函数体里面是可以修改的,否则就不能修改。上面的代码,如果没写mutable就会编译报错,原因就是在函数体内对x进行了自增操作。如果是按引用捕获,默认就是可以修改的,不需要写mutable。上面的代码返回值类型也可以不写,编译器自己会推导出return语句的值就是返回值类型。

通过一个等效的实现可能更好理解一些。上面代码的lambda表达式等效于:

1
2
3
4
5
6
7
8
9
10
11
class LambdaFunc {
public:
LambdaFunc(int xx) : x(xx) {}
bool operator()(int y) {
bool result = x == y;
cout << x++;
return result;
}
private:
int x;
}

如果lambda表达式没有写mutable,就相当于LambdaFunc的成员变量声明为const。

对于以下语句:

1
auto f = [x](int y) mutable -> bool { bool result = x == y; cout << x++; return result; };

相当于编译器创建了一个LambdaFunc临时对象,并且赋值给变量f。

如果我们把这个表达式稍微改动一下,变成这样:

1
auto f = [&x](int y) -> bool { bool result = x == y; cout << x++; return result; };

相当于:

1
2
3
4
5
6
7
8
9
10
11
class LambdaFunc {
public:
LambdaFunc(int& xx) : x(xx) {}
bool operator()(int y) {
bool result = x == y;
cout << x++;
return result;
}
private:
int& x;
}

关于捕获,还有以下几种形式:

  • [=] 表示捕获lambda表达式作用域范围内的所有变量,且都是值捕获;
  • [&] 表示捕获lambda表达式作用域范围内的所有变量,且都是引用捕获;
  • [=,&x] 表示捕获lambda表达式作用域范围内的所有变量,只有x是引用捕获,其余都是值捕获;
  • [&,=x] 表示捕获lambda表达式作用域范围内的所有变量,只有x是值捕获,其余都是引用捕获。

应用于标准库

假设有如下代码:

1
2
auto cmp = [](int x, int y){return x < y; };
std::set<int, decltype(cmp)> myset;

这段代码会报出编译错误,错误信息显示lambda表达式是没有默认构造函数的。因为myset在构造时会构造一个类型为decltype(cmp)的比较器,而它时没有默认构造函数的。同时lambda表达式也没有赋值运算符,意味着不能将一个类型为lambda表达式的变量赋值给另一个变量。

以上代码正确的写法应该是:

1
2
auto cmp = [](int x, int y){return x < y; };
std::set<int, decltype(cmp)> myset(cpm);