定制操作
很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或==运算符进行比较。
如sort算法默认使用元素类型的<运算符进行升序排列。但是我们希望的排序顺序与<所定义的顺序不同,或者我们的序列可能保存的是未定义的<运算符的元素类型(如Sales_data)。这两种情况下都需要重载sort的默认行为,使用这些算法的额外版本,允许我们提供自己定义的比较运算符来代替默认运算符。
有的算法接收谓词作为其参数,有的算法接收可调用对象作为其参数。
1.向算法传递函数
想要使用重载版本的算法来代替算法的默认形式,则需要向算法传递一个叫谓词的参数。
谓词是一个可调用的表达式,其返回结果是bool类型。
①标准库算法所用的谓词只能接收一个参数或者两个参数,因此可以分为两类:一元谓词和二元谓词:
- 一元谓词:只接受一个参数的谓词。
- 二元谓词:只接受两个参数的谓词。
②算法的谓词模式包括以下五种:
-
函数谓词:通过传递函数名,匹配谓词。
-
函数指针谓词:建立一个函数指针,使用指针代替函数名,类似函数谓词。
-
lambda表达式谓词:可匹配一元、二元谓词,如果希望的操作需要更多的参数,超出算法对谓词的限制,可以通过捕获列表达到效果。
-
函数对象(重载函数调用运算符()的自定义类型的对象)谓词:函数对象使用函数调用运算符()时看起来很像一个函数调用,因此又叫仿函数。返回bool类型的仿函数叫函数对象谓词。
-
库定义的函数对象谓词:标准库定义的函数对象,充当算法的谓词。
如:find_if函数接收一元谓词,查找第一个长度大于5的元素。如果有匹配的元素,则返回这个元素的迭代器,否则,返回尾后迭代器。
bool greaterFive(string &s) {
return s.size() > 5 ? true : false;
}
int main() {
vector<string> vec{ "abc", "bcde", "aaaaaa" };
auto iter = find_if(vec.begin(), vec.end(), greaterFive);
cout << *iter << endl;
return 0;
}
//输出结果为:"aaaaa"
2.lambda表达式
算法只接受一元谓词和二元谓词,我们传递给算法的谓词必须严格接收一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制,这时候就可以用lambda表达式来解决这个问题。
如:对给定一个字符序列,对其进行字典降序,并返回第一个大于等于给定长度sz的元素的迭代器。
很显然,如果用函数作为谓词的话,需要3个参数,超出了算法对谓词的限制。
2.1介绍lambda
①可调用对象
可以向算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。即,如e是一个可调用的表达式,则可以写成e(args)的形式,args表示一个参数列表。
②lambda的定义
一个lambda表达式表示一个可调用的代码单元,相当于一个未命名的内联函数。与任何函数类似,lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部(函数不能定义在函数内)。
lambuda表达式具有如下形式:
[capature list](parameter list)->return type {function body}
其中,capture表示捕获列表、prarmeter list表示参数列表、return type表示尾置返回类型、function body表示函数体。参数列表和返回类型可以省略,但必须永远包含捕获列表和函数体。
如:定义一个可调用对象f,不接受任何参数,返回42。将lambda表达式赋值给一个对象,这个对象就变成了一个可调用对象。
auto f = [] {return 42;}
cout << f() << endl;
③lambda省略参数列表和返回类型的情况
在lambda中忽略参数列表等价于一个空参数列表。
如果忽略返回类型,lambda根据函数体中的代码推断返回类型:
- 如果函数体只是一个return语句,则返回类型从返回的表达式类型推断出来。
- 否则,返回类型为void。如果lambuda的函数体中包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
- 只有lambda返回bool类型的时候才能用作谓词。
④向lambda传递参数(调用lambda)
调用一个lambda与函数调用类似,但不同的是lambda不能有默认参数,因此,调用的实参数目永远与形参相等。调用lambda的方法一般有两种:
- 将lambda赋值给一个可调用对象。
如:返回两个字符串中较小的一个
auto f = [](const string &s1, const string &s2) {
return s1.size() < s2.size() ? s1 : s2;
}
string s = f("abc", "abcd");
cout << s << endl;
//结果为:abc
- 作为谓词直接作为算法的参数,算法运行时会调用。
如:用sort算法按字符串长度进行升序。
vector<string> s{"kbc", "abdee", "akzz", "op"};
sort(s.begin(), s.end(),
[](const string &s1, const string &s2)
{return s1.size() > s2.size();});
for (auto s : s) {
cout << s << " ";
}
//结果为:abdee akzz kbc op
⑤使用捕获列表
一个函数不能出现在另一个函数中,而lambda可以出现在一个函数中,使用其局部变量,但它只能使用捕获列表中的局部变量。
如:我们希望lambda表达式能将输入序列中每个string的长度与函数中的sz进行比较。查找第一个长度不小于sz的迭代器。
vector<string> s{"abc", "abcd", "abcde"};
int sz = 3;
auto it = find_if(s.begin(), s.end(),
[sz](const string &s){return s.size() > sz;});
cout << *it << endl;
//结果为:abcd
又如:打印所有长度大于sz的字符串。
vector<string> s{"abc", "abcd", "abcde"};
int sz = 3;
for_each(s.begin(), s.end(), [sz](const string &s)
{if (s.size() > sz) cout << s << endl; });
//结果为:abcd
3.lambda捕获
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。即:当向一个算法函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是编译器生成的类类型的未命名的对象。
类似函数的参数传递,变量的捕获方式也可以是值或者引用。
①值捕获
采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda被创建时拷贝,而不是调用时拷贝:
如:
void func() {
int v = 42;
auto f = [v]{return v;};
v = 0;
int j = f();
//j为42,如果lambda被调用时再拷贝捕获的对象,那j就应该时0.
}
②引用捕获
定义lambda时也可以采用引用捕获的方式。如:
void func2() {
int v1 = 42;
auto f2 = [&v1]{return v1;};
v1 = 0;
auto j = f2();
cout << j << endl;
//j为0,因为f2捕获的是一个引用,因此,当v1变得时候,捕获得值也会改变。
}
注意:当以引用方式捕获一个变量时,必须保证在lambda执行时变量时存在得。
③隐式捕获
根据函数体中代码推断我们要使用哪些变量。捕获列表用=或&来说明采用哪种捕获方式。
④可变lambda
对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望改变捕获的变量的值,就必须在参数列表后面加上关键字mutable,因此可变lambda不能省略参数列表。
如:
void func3() {
int v = 42;
auto f = [v] () mutable {return ++v;};
auto j = f();
cout << j << endl;
}
//结果为:43
对于一个引用捕获的变量是否可以修改依赖此引用指向的是一个const还是一个非const类型。
如:
void func4() {
int v = 42;
//如果是const int v = 42;则不可改变v的值
auto f = [&v] {return ++v;};
int j = f();
cout << j << endl;
}
//结果为:43
4.指定lambda返回类型
如果一个lambda函数体包含return之外的任何语句,则lambda默认返回void,而被推断返回void的lambda不能返回值。
如:
vector<int> vec{ 1, -1, -5, -10, 5 };
vector<int> vec2;
transform(vec.begin(), vec.end(), back_inserter(vec2), [](int a)
{if (a < 0) return -a; else return a;);
//错误:lambda推断返回void,不能返回值,应写成
transform(vec.begin(), vec.end(), back_inserter(vec2), [](int a)
{ return a < 0 ? -a : a;);
当我们需要为一个lambda自定义返回类型时,必须使用尾置返回类型。
如:对上述进行改进
vector<int> vec{ 1, -1, -5, -10, 5 };
vector<int> vec2;
transform(vec.begin(), vec.end(), back_inserter(vec2), [](int a)->int
{if (a < 0) return -a; else return a;});