简述泛型算法之 二lambda表达式
背景:
我们传递的算法必须严格接受一个或两个参数,但是有时我们希望进行的操作需要更多的参数,超出了算法对谓词(predicate)的限制。
在上篇博客简述泛型算法之 一认识算法末尾提到,要分割的长度大于n,普通方法无能为力。这个时候,就需要lambda表达式”登场了”。
lambda表达式
我们可以为任何一个算法传递任何类别的可调用对象。
可调用对象:对于一个对象或者表达式,如果能够对其使用调用运算符,则称它为可调用的;如果e可调哟个,则可以写代码e(args) arges为参数列表
之前学过的可调用对象:函数,函数指针;
lambda表达式:表示一个可调用的代码单元。可以理解为一个未命名的内敛函数,与任何函数类似,一个lambda具有返回类型,参数列表,函数体。
但与函数不同的是,lambda可以定义在函数内部。一个lambda表达式具有如下形式:
[capture list](parameter list) -> return type {funtion body}
capture list(捕获列表)是一个lambda对象所在函数中定义的局部变量的列表;
return type, parameter list, function body 与普通函数一样
但与普通函数不同的是,lambda必须使用尾置返回(->)来指定类型
我们可以忽略函数参数列表,但是必须永远包含捕获列表和函数体。
lambda基本用法
//定义一个可调用对象f,不接受参数;
auto f1 = [] { return 34; };
//调用方式与普通函数一样,使用函数调用运算符
cout << f1() << endl;
//传递参数:与普通函数不同,lambda不能有默认实参
//编写一个与isShorter功能完全一样的lambda
auto f2 = [](const string& a, const string& b) { return a.size() < b.size(); };
vector<string> vss{"the", "quick", "red", "fox", "jumps", "over", "the","slow", "red", "turtle"};
//使用lambda调用stable_sort
stable_sort(vss.begin(), vss.end(), f2);
stable_sort(vss.begin(), vss.end(),
[](const string& a, const string& b) {
return a.size() < b.size();
});
lambda捕获变量
虽然lambda可以定义在函数内,使用其局部变量,但是它只能使用那些明确指明的变量。
lambda通过将局部变量包含在其捕获列表[]中来指出将会使用这些变量
//值捕获
{
size_t sz = 43;
//值捕获即值的拷贝,且值拷贝发生在lambda定义时,而非调用时,所以sz不会被改变,且lambda内部不能改变值捕获的变量
auto f = [sz] {return sz;};
sz = 0;
cout << f() << endl; //43 因为是拷贝
//lambda内部默认不能改变值捕获的变量,改变的方法:加上mutable (有没有觉得很熟?哪里见过?见后文)
sz = 43;
auto g = [sz]()mutable { return ++sz; };
cout << g() << endl;
}
//引用捕获
//引用捕获相当于定义了一个引用,因此必须遵循引用相关的规则:即必须引用到一个已经存在的变量
{
size_t sz = 43;
//引用捕获可以改变捕获的变量,而且内部也可以改变;
//引用捕获有时是必要的,因为有些类型无法拷贝:iostream, unique_ptr
auto f = [&sz] { return ++sz; };
sz = 0;
cout << f() << endl; //0 因为是引用
}
//显示捕获,隐式捕获
{
size_t sz = 0;
//= 标识隐式值捕获
auto f = [=] { return sz; };
//& 标识隐式引用捕获
auto g = [&] { return sz; };
cout << f() << g() << endl;
}
隐式,显式,值,引用捕获混合使用
当我们混合使用隐式和显式捕获时,捕获列表中第一个元素必须 是& 或者 =,此符号指定了默认捕获方式
而且,显式捕获的变量和隐式捕获的变量必须采用不同的方式(值和引用)
即不能出现此种情况 [&, &c] or [=, =c];
只能出现此种情况: [&, =c] or [=, &c];
{
int c = 0;
//cout:隐式引用捕获; c:显式值捕获
auto f = [&, c] { cout << c << endl; };
//c:隐式值捕获 cout:显式引用捕获
ostream& os(cout);
auto g = [=, &os] { os << c << endl; };
}
综上所述:尽可能保持lambda的变量捕获简单化,尽量减少捕获的数据量;如果可能,尽量避免捕获引用或指针
指定lambda返回值
ex:算法对输入序列中每个元素调用可调用对象,并将结果写回目的位置。
transform(v.begin(), v.end(), v.begin(),
[](int i) {
return i < 0 ? -i : i;
});
但如果写成这样,会产生编译错误
transform(v.begin(), v.end(), v.begin(),
[](int i) {
if (i < 0) {
return -i;
} else {
return i;
}
});
(C++11会产生错误,但C++14不会;笔者的xcode支持C++14,所以这样写也不报错)
改成如下形式,指定返回值则不产生错误
transform(v.begin(), v.end(), v.begin(),
[](int i) -> int {
if (i < 0) {
return -i;
} else {
return i;
}
});
小例子:求大于等于一个给定长度的单词有多少,使程序只打印大于等于给定长度的单词
//消除重复单词
void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
//只打印大于等于给定长度(sz)的单词
void biggies(vector<string>& words, vector<string>::size_type sz) {
elimDups(words);
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b) {
return a.size() < b.size();
}); //lambda表达式代替上篇博客的函数指针
auto wc = find_if(words.begin(), words.end(),
[sz](const string& a) {
return a.size() >= sz;
}); //lambda表达式
auto count = words.end() - wc;
cout << count << " ";
for_each(wc, words.end(),
[](const string &a) {
cout << a << " ";
});
cout << endl;
}
上述代码仅仅用于应用lambda测试,也有更简单的办法
void biggies_2(vector<string>& words, vector<string>::size_type sz) {
vector<string>::size_type count = 0;
for (auto iter = words.begin(); iter != words.end(); ++iter) {
if (iter->size() >= sz) {
++count;
cout << *iter << " ";
}
}
cout << count << endl;
}
笔者的代码没有修改原vector,而书中代码修改了vector,所以视情况而选择方法。(此处仅仅是学习lambda)
lambda实现原理
当编写一个lambda时,编译器生成一个与lambda对应的未命名的类的未命名对象。在lambda产生的类中,含有一个重载的函数调用运算符。
ex1:没有捕获变量
stable_sort(vss.begin(), vss.end(),
[](const string& a, const string& b) {
return a.size() < b.size();
});
其行为类似于下面这个类的未命名对象
class ShorterString {
public:
bool operator()(const string& s1, const string& s2) const {
return s1.size() < s2.size();
}
};
产生的类只有一个函数调用运算符成员。为何默认情况下不能修改它捕获的变量?因为该函数默认是const。
此时,通过该类替代lambda后,可以重写函数如下:
stable_sort(vss.begin(), vss.end(), ShorterString());
注意: ShorterString()
为类构造的临时对象;
ex2:捕获变量
当通过引用捕获时,将由程序负责确保引用所引的对象确实存在。因此编译器可以直接使用该引用而无需在类中存储数据成员。
相反,当通过引用捕获时,lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其捕获的变量初始化数据成员。
auto wc = find_if(v.begin(), v.end(),
[sz](const string &a) {
return a.size() >= sz;
});
该lambda表达式产生类形如:
class SizeTmp {
public:
SizeTmp(size_t n):sz(n) {}
bool operator()(const string& s) const {
return s.size() >= sz;
}
private:
size_t sz;
};
到这里,就差最后一个困惑了?
值捕获不能修改捕获的变量,如果要修改则必须加上mutable?why?
当我问完问题,答案也出来了。因为有一种成员变量即使是const成员函数,也能修改。在定义的时添加mutable。
private:
mutable size_t sz;
};
所以,在之前提到过,要想修改捕获的变量,也只需要加上mutable.
lambda表达式产生的类不含默认构造函数,赋值运算符以及默认析构函数;它是否有默认拷贝/移动函数通常要视捕获的数据成员类型而定。
bind
虽然本文中谈到lambda简洁且强大,但在某些时候仍然有其局限性,最后就得使用”终极武器”bind。详情见下篇简述泛型算法之 三bind 参数绑定