Lambda表达式


持续更新,补充C++新增feature,目录结构遵循《C++ Primer》

Lambda 表达式

C++ 11引入了一种新的类型称为lambda表达式,一个lambda表达式代表一个可调用的代码单元,可以将其理解为一个匿名的内联函数。Lambda表达式的定义为:

\[[capture \thinspace list] \thinspace (parameter \thinspace list) \enspace \rightarrow \enspace return \thinspace type \enspace \{ \enspace function \thinspace body \enspace \}\]

其中$capture \thinspace list$(捕获列表)是一个lambda表达式中定义局部变量的列表(通常为空)。参数列表和返回类型可以忽略,捕获列表必须存在。

auto f = []{return 42;}

捕获列表的规则

[] 不适用任何外部变量
[=] 隐式捕获,=必须放在最前面,以传值的形式使用外部所有变量,值不可以被修改
[&] 隐式捕获,&必须放在最前面,以引用的形式使用所有外部变量,引用可以修改
[x,&y] x以值传递(拷贝)形式引入, y以引用形式引入
[=,&x,&y] x,y以引用形式使用,其余变量以传值形式引入
[&,x,y] x,y以传值形式引入,其余变量以引用形式使用

对于捕获列表,只用于捕获所在函数内的局部变量,对于全局符号,或者static变量,则无需出现在捕获列表,里,例如下面代码中的cout属于全局符号,不需要捕获:

vector<string> words{"C++","Java","Ruby"};
for_each(words.begin(),words.end(), [](const string& s){
  cout<<s<<endl;
});

当定义了一个lambda对象时,编译器做了这么几件事:

  1. 创建了一个与lambda对应的新类型(未命名,它实际上是一个Functor重载了()运算符,在运算符重载一节还会分析)。当向一个函数传递lambda时,同时定义了一个新类型和该类型的一个对象
  2. 在生成的新类型中,捕获列表作为该类的数据成员在lambda对象被创建时初始化
  3. 如果使用auto定义lambda变量时,实际上定义了一个从lambda生成的类型的对象

尽量保持lambda捕获的变量简单化,避免捕捉指针或者引用。如果捕获了引用,应确保在lambda函数体执行时该引用仍有效。

mutable lambda

如果一个参数以值捕获的方式被lamda所引用,那么它的值是不能被修改的,如果要求改被捕获的值,需要使用mutable关键字。

void fcn3(){
  size_t v1 = 42; //局部变量
  //f可以改变v1
  auto f = [v1]()mutable { return ++v1; }
  v1 = 0;
  auto j = f(); //j = 43;
}

如果局部变量是以引用的形式被捕获,则不需使用mutable关键字,能否修改被捕获的变量要看引用的类型,是否是const

bind函数

考虑这样一个问题,标准库中的find_if函数接受三个参数,前两个是迭代器对象用来确定查找返回,第三个参数是接受一个参数的函数指针,我们可以使用lambda表达式来作为第三个参数:

find_if(words.begin(),words.end(),[sz](const string& s){
  return s.size() > sz;
});

上个面的lambda表达式实际上是接受了2个参数sszsfind_if传入lambda表达式的,sz是由lambda捕获的,函数的作用是比较s.size()sz。这个问题也可以使用函数指针,我们可以定义这样一个函数:

bool check_size(const string& s, size_t sz){
  return s.size() > sz;
}

这里便出现了一个问题,check_size接受两个参数,显然不满足find_if的要求,因此便不能将check_size当做参数传递给find_if

这时我们需要使用bindcheck_size进行一下包装,C++ 11中对bind的定义为:

auto newCallable = bind(callable, arg_list);

其中newCallable是一个可调用对象,arg_list是一个callable接受的以逗号分隔的参数列表,当调用newCallable时,newCallable会调用callable,并传入arg_list作为参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个自然数,代表“占位符”,例如_1表示newCallable的第一个参数,_2表示第二个参数,以此类推。

接下来我们可以用bind来包装check_size来生成一个可以调用check_size的对象,如下:

auto check = bind(check_size,_1,6);
string s = "hello";
check(s); //check(s)会调用check_size(s,6);

上述check函数只接受一个参数_1,该参数对应check_size的第一个参数,即const string&类型,这样就可将其应用于find_if函数:

using namespace std::placeholders
find_if(words.begin(),words.end(),bind(check_size,_1,sz));
  • 使用placeholder

占位符_n定义在std::placeholders的命名空间中,使用前要进行using声明。_n还可以用来调整函数接受参数的顺序,例如

auto g = bind(f,a,b,_2,c,_1);

g的第二参_2数对应f的第三个参数,g的第一个参数_1对应f的最后一个参数,例如:

g(X,Y) //会调用 f(a,b,Y,c,X);
  • 绑定引用参数

对于bind的另一个问题是如何绑定引用类型的参数,除占位符以外的参数,默认是值传递,有些情况参数无法拷贝,只能取引用,这时需要使用ref函数(定义在functional),ref返回一个对象,包含给定的引用,此对象是可以拷贝的,类似的还有cref,生成一个保存const引用类的对象。

void some_func{
  ...
  //os是一个局部变量,引用一个输出流
  //c是一个局部变量,类型为char
  //使用lambda表达式来捕获引用类型
  for_each(words.begin(),words.end(),[&os, c](const string &s){
      os<<s<<c;
  });
} 

对上述代码使用函数进行改写后,使用bind

ostream& print(ostream& os, const string& s, char c){
  return os<<s<<c;
}

void some_func(){
  ...
  //os是一个局部变量,引用一个输出流
  //c是一个局部变量,类型为char
  //使用ref来返回一个引用类型的可拷贝对象
  for_each(words.begin(),words.end(),bind(print,ref(os),_1,c));
}

STL中的bind1st, bind2nd函数已被废弃