介绍与引入
最近在阅读C++ Concurrency in Action 2rd edition 以学习C++的多线程特性,发现许多地方都使用了Lambda函数,因此就附录里的内容做一些笔记。
Lambda函数是C++11标准中新增的一个特性,允许在需要使用的时候才进行定义,这种语法糖的特性大大简化了代码复杂度,在某些时候特别有用。其语义可以用来快速地表示可访问的变量,而非使用类中函数来对成员变量进行捕获。
基本的Lambda表达式
Lambda表达式最简单的形式,便是一个self-contained函数,其没有输入参数,仅仅依赖全局变量与函数,甚至可以没有返回值。这样的lambda表达式是一系列由大括号包裹的语句,并且以一对方括号[]
作为前缀(称为lambda introducer)。
[]{ // Lambda表达式以[]开始
do_stuff();
do_more_stuff();
}(); // 表达式结束,可以直接调用
在这个例子中,表达式通过后面的()
调用,不过这种方式不常见。如果你要直接调用,直接写就是了,不必再通过lambda表达式。更加常见的用法是将lambda表达式作为一个函数模版的参数,该参数是一个可调用的对象,这种情况下的lambda表达式就可能需要传参或是返回值。如果需要给lambda函数传递参数,在lambda introducer后附上参数列表即可。
以下是一个具体的例子,通过lambda函数对vector中的元素依次打印输出
std::vector<int> data=make_data();
std::for_each(data.begin(),data.end(),[](int i){std::cout<<i<<"\n";});
返回值也很简单,如果lambda函数的函数体只包含一个return
语句,那么返回值的类型就是lambda函数的返回值类型 。例如,使用lambda函数来等待std::condition_variable
中的flag被设置(这里都用多线程的例子,等待函数未来会填坑(也许))。
std::condition_variable cond;
bool data_ready;
std::mutex m;
void wait_for_data(){
std::unique_lock<std::mutex> lk(m);
cond.wait(lk,[]{return data_ready;});
}
这里有全局变量cond
,data_ready
与m
。传递给cond.wait
的lambda表达式的返回值从data_ready
的类型推断,因此返回值是布尔类型。每当条件变量从等待中苏醒时,他就会调用lambda表达式与mutex
锁,并且对于wait()
的调用只有data_ready
为真时才会返回(挖个坑)。
如果lambda函数的函数体包含多个return
语句,那此时就必须显式地指定返回类型(当然单个return
语句也可以指定)。返回值类型由参数列表()
后接着的->
指定。这也意味着,即使lambda函数不需要传参,若要显示指定返回值类型,也要写(空)参数列表()
。 则上述的例子可以写为:
cond.wait(lk,[]()->bool{return data_ready;});
通过指定返回类型,可以展开lambda以打印log或执行一些更复杂的逻辑:
cond.wait(lk,[]()->bool{
if(data_ready){
std::cout<<”Data ready”<<std::endl;
return true;
}else{
std::cout<<”Data not ready, resuming wait”<<std::endl;
return false;
}
});
虽然像这样简单的lambda函数已经可以大大简化代码,但它的真正威力到它们捕获局部变量时才会真正显现出来。
引用局部变量的lambda函数
使用空lambda inrocuder[]
的函数不能引用任何的局部变量,其只能使用全局变量或者通过函数传参。如果想要访问全局变量,就需要捕获(capture)它。捕获全局变量最简单的方式就是通过[=]
将作用域中所有的变量全部捕获,则lambda函数在创建时就会拷贝局部变量的一份副本以供访问。考虑以下代码:
std::function<int(int)> make_offseter(int offset){
return [=](int j){return offset+j;};
}
每次对make_offseter
的调用都会通过std::function<>
函数包装返回一个新的lambda函数体,该函数添加了对于参数的偏移。
int main()
{
std::function<int(int)> offset_42=make_offseter(42);
std::function<int(int)> offset_123=make_offseter(123);
std::cout<<offset_42(12)<<”,“<<offset_123(12)<<std::endl;
std::cout<<offset_42(12)<<”,“<<offset_123(12)<<std::endl;
}
/*
输出为
54,135
54,135
*/
这是最安全的局部变量捕获形式:拷贝所有的局部变量,因此可以返回一个lambda函数并在原来函数的范围之外调用它。
但这并不是唯一的方法;我们也可以选择通过引用来捕获所有局部变量。在这种情况下,一旦其引用的变量由于退出所属的函数或块作用域而销毁时,调用lambda函数就是未定义行为,就像引用在其他情况下已经被销毁的变量一样。以下介绍关于[&]
对局部变量的引用。
int main() {
int offset = 42; // 1
std::function<int(int)> offset_a = [&](int j) { return offset + j; }; // 2
offset = 123; // 3
std::function<int(int)> offset_b = [&](int j) { return offset + j; }; // 4
std::cout << offset_a(12) << "," << offset_b(12) << std::endl; // 5
offset = 99; // 6
std::cout << offset_a(12) << "," << offset_b(12) << std::endl; // 7
}
/*
输出为
135,135
111,111
*/
上述两次打印的结果不同,这就是引用所带来的。当然,C++的语法支持你可以在同一个lambda表达式中同时使用拷贝与引用的特性,通过调整[]
中的参数来选择特定的类型进行局部变量的捕获。可以比较下面三个例子的区别。
- 如果想要拷贝大部分变量,而部分引用,可以在
=
后加上部分参数的引用[=, &j, &k]
int main() {
int i=1234,j=5678,k=9;
std::function<int()> f=[=,&j,&k]{return i+j+k;};
i=1;
j=2;
k=3;
std::cout<<f()<<std::endl;
}
/*
输出为
1239
*/
- 相反的,如果想要引用大部分变量而部分拷贝,只需在
&
后加上部分拷贝参数[&, i, j]
int main(){
int i=1234,j=5678,k=9;
std::function<int()> f=[&,j,k]{return i+j+k;};
i=1;
j=2;
k=3;
std::cout<<f()<<std::endl;
}
/*
输出为
5688
*/
- 当然,如果不用
=
或者&
,则可以实现部分局部变量的捕获。
int main() {
int i=1234,j=5678,k=9;
std::function<int()> f=[&i,j,&k]{return i+j+k;};
i=1;
j=2;
k=3;
std::cout<<f()<<std::endl;
}
/*
输出为
5682
*/
最后一种方式中,如果lambda函数体中出现了没有被捕获的变量就会引起编译错误,因此当选择这种方式时,如果包含lambda的函数是类的成员函数,那么需要对类成员变量的访问特别注意。成员变量无法直接捕获。如果在函数中需要捕获类的成员变量,则需要捕获this
指针。下面的例子展示了这种情况:通过捕获this
指针访问类的成员变量some_data
。
struct X {
int some_data;
void foo(std::vector<int> &vec) {
std::for_each(vec.begin(), vec.end(),
[this](int &i) { i += some_data; });
}
};
lambda函数在并发的上下文中很有用,可以作为std::condition_variable::wait()
和 std::packaged_task<>
的谓词,也可以作为 线程池中线程函数std::thread
的构造函数,或者是作为并行算法的实现在parallel_for_each()
中被使用。