C++的这个系列接着续上,这次聊聊lambda表达式。
C++11中首次引入了lambda表达式。通俗的来说,就是可以方便的定义和创建一个内嵌的匿名函数。我们都知道在C++中,一个普通的函数可以被这样定义:
bool compare(int a, int b) {
return a > b;
}
这样的标准定义方式在对于查找一段逻辑的调用栈,或者不同层级的逻辑相互解耦的时候是很好的写法。但当我们需要将这个函数的值传递给另外一个算法或者异步函数,特别是函数中的代码量很少的时候,lambda表达式也是一种不错的选择。
例如此时,我们需要自定义std::sort的比较函数时,调用普通函数的做法会是这样:
std::sort(arr.begin(), arr.end(), compare);
而在使用了lambda表达式之后:
std::sort(arr.begin(), arr.end(),
[](int a, int b) {
return (std::abs(a) < std::abs(b));
});
可以看到,这里我们直接用lambda表达式创建的匿名函数替换了原先独立声明的compare函数,语法上简洁了许多。
基本语法
知道了我们会在什么情况下选择lambda表达式,下面我们来看一下它具体的语法。
首先来看一下最简单的例子
auto helloWorld = [] { std::cout << "Hello, world!" << std::endl; };
helloWorld();
这个例子中的lambda表达式没有任何的参数和返回值,只是执行了函数体中的内容。那么如果我们想要加上参数和返回值,就需要跟普通的函数一样,在括号中包括我们想要的参数,并在->之后加上返回值类型。
int cnt = wordCount("User"); // return 4
capture子句的原理和使用
每当定义一个lambda表达式后,编译器会自动生成一个重载了 () 运算符的匿名类,称为闭包类型(closure type)。在runtime,表达式会返回一个匿名的闭包实例。所以我们可以说,之前的例子中我们定义的每一个lambda表达式都是一个闭包。
闭包的一个特性就是,可以通过传递值或者引用的方式捕捉到作用域中的变量。具体到语法上,就是lambda表达式前面的方括号,也叫capture子句。例子如下:
int base_length = 10;
auto wordCount = [base_length](std::string username) -> int { return username.length() + base_length; };
int cnt = wordCount("User"); // return 14
总体来说,lambda的capture字句大体有一下几类:
- [&]:通过默认引用的方式捕获所有的变量
- [=]:通过默认值的方式捕获所有的变量
- [var]:通过值的方式捕获变量var
- [&var]:通过引用的方式捕获变量var
- [=, &var]:对于除var以外的变量,默认通过值捕获,var用引用捕获
- [&, var]:对于除了var以外的变量,默认通过引用捕获,var用值捕获
- [this]:捕获当前对象(本质是复制了指针)
[&]或[=]使用的问题
-
[&] Dangling Reference
对于默认引用捕获来说,当该引用对应的变量被销毁之后,其指向的值不再是原先我们预期的值,因此我们的lambda函数就会出现不可预知的行为。例如std::function<int(int)> test(int x){ lambdafunc = [&](int y) -> int { return x + y; }; }
在这个例子中,lambdafunc的调用中,x已经出了它的作用域,因此调用它会产生没有意义的结果。
- [=]
使用传值的捕获可以避免以上的问题,但是这样依然有问题。例如在一个类中,使用[=]会将this指针一并捕获,相当于就是以引用的方式捕获了这个类的对象。那么同样的,当这个类被析构之后,再次调用这个表达式依然会出现dangling reference的问题。因此[=]的不安全点主要是在于,其将指针进行复制,本质上还是传递了一个引用。
[var]和mutable的使用
经过上文的叙述,我们知道一个变量可以通过传值和传引用的方式被捕获。理所应当的,当这个值是以传引用的方式被捕获的时候,我们可以修改它的内容,这一点和函数参数的引用是类似的。那么假如我们使用的是以传值的方式进行捕获,还可以进行修改吗?
答案是可以的,但是需要搭配上mutable关键字。例如:
auto func = [x](int y) mutable -> int { x = x * 2; return x + y; };
通过mutable关键字,使得其在生成的闭包类中的operator()的重载不再是const类型,自然就可以进行赋值了。
这次简单介绍了一下lambda表达式的使用,下一篇聊聊右值引用。