C++ Primer第十章②
C++ Primer
泛型算法
定制操作
很多算法都会比较元素,默认情况下,这类算法使用元素类型的<或==运算符来完成比较,但是有两种情况可能需要我们多做一些工作:
- 我们希望的排序顺序与<定义的顺序不同
- 我们保存的元素没有定义<运算符 在这两种情况下,我们需要重载sort的默认行为。
向算法传递参数
我们现在要完成这样一个任务:对一个vector<string>按照其单词长度排序,长度相同的再按照字典序排列。
对于这个任务,我们要定义自己的比较规则(因为默认的规则是字典序),我们要使用一个sort的重载版本,这个版本接受第三个参数,这个参数是一个谓词。
谓词:一个可调用的表达式,返回结果是一个能用作条件的值,谓词可以接受一个或两个参数,分别称为一元谓词和二元谓词。
好了,我们来完成这项任务:
//之前的那个函数,字典序排列且去重
void Unique(vector<string> &words)
{
sort(word.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.eraser(end_unique, wors.end());
}
//自定义函数,用长度比较,待会作为第三个参数,谓词
bool isShorter(const string &s1, const string &s2)
{
return s1.sie() < s2.size();
}
int main()
{
vector<string> words; //假装有内容
Unique(words); //先字典序
stable_sort(words.begin(), words.end(), isShorter); //再按长度
//用stable_sort稳定排序是为了让长度相同的单词还是保持字典序不变
for(const auto &s : words)
{
cout << s << " ";
}
cout << endl;
return 0;
}
lambda表达式
这东西我一直觉得很难,希望这次能借这个机会再好好学习一下,嫌弃我说不清楚的话建议看原书。。。
首先,我们为什么要有这个表达式呢?还是跟之前的谓词有关,因为谓词最多只能接受两个参数,但我们有时候希望进行的操作需要更多的参数,我们来举个例子,现在我们要修改上面写的程序,求大于等于一个给定长度的单词有多少,打印输出这些单词,我们先来写一下这个函数的框架,看看会有什么问题用目前的知识无法解决的:
void biggies(vector<string> &words, vector<string>::size_type sz)
//size相当于unsigned int
{
//开始的步骤和之前一样
Unique(words);
stable_sort(words.begin(), words.end(), isShorter);
//接下来要做的是获取一个迭代器,指向第一个满足size>sz的元素
//然后就可以从这个元素开始依次打印输出了
}
所以,我们现在的问题就是要在一个vector中寻找第一个大于等于给定长度的元素。
这个问题看似简单,我们来分析一下:
标准库中有一个算法find_if用来查找第一个具有特定大小的元素,它接受三个参数,前两个是一对迭代器表示输入范围,第三个参数是一个一元谓词,find_if算法对输入序列中每个元素调用这个谓词,它返回第一个使谓词返回非0值的元素。
看着挺好用的,那么问题在哪呢?问题就在第三个参数是一元谓词,我们在编写这个谓词函数(我是这么叫的)的时候,肯定要传给它两个参数,一个string和一个长度,这就有问题了,因为人家是一元谓词,无法接受两个参数,所以啊,here
comes lambda.
介绍lambda(兰木达)
一个lambda表达式表示一个可调用的代码单元,我们可以理解为未命名的内联函数,一个lambda表达式具有如下形式:
[capture
list] (parameter list) -> return type {function body}
我们熟悉的有形参列表,返回类型(必须尾置),函数体
这个捕获列表是lambda表达式所在函数中定义的局部变量的列表,要好好理解这句话,这句话对后面理解lambda的作用很大。
我们可以忽略形参列表和返回类型,但必须包含捕获列表和函数体:
auto f = [] {return 42;} //定义了一个可调用对象f,不接受参数,返回42
cout << f() << endl; //打印42
向lambda传递参数
lambda不能有默认参数,这是规定。
作为一个带参数的lambda例子,我们来写一个与isShorter函数完成相同功能的lambda:
[] (const string &a, const string &b){ return a.szie() < b.szie();}
空捕获列表表明此lambda不使用它所在的函数中的任何局部变量,我们可以使用此lambda来调用stable_sort函数:
stable_sort(
words.begin(), word.end(),
[] (const string &a, const string &b){ return a.szie() < b.szie();}
);
使用捕获列表
不要忘了我们为什么要引出lambda这个概念,我们现在就来解决这个问题:编写一个可以传递给find_if的可调用表达式,我们希望这个表达式能将输入序列中的每个string的长度与biggies函数中的sz参数进行比较。 我们是怎么来传递多余信息呢?答案就是捕获列表,一个lambda通过将局部变量包含在其捕获列表中来使用:
[sz] (const string &a){ return a.szie() >= sz; };
//sz是lambda表达式所在函数的变量,是lambda捕获来的
调用find_if
使用这个lambda就可以搞定了:
auto wc = find_if(wors.begin(), wors.end(),
[sz] (const string &a){ return a.szie() >= sz; });
完整的biggies
void biggies(vector<string> &words, vector<string>::size_type sz)
{
Unique(words);
stable_sort(
words.begin(), word.end(),
[] (const string &a, const string &b){ return a.szie() < b.szie();}
);
auto wc = find_if(wors.begin(), wors.end(),
[sz] (const string &a){ return a.szie() >= sz; });
for_each(wc, words.end(), [](const string &s){cout << s << " ";});
cout << endl;
}
lambda捕获和返回
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型,目前可以这样理解,当向函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
捕获方式
捕获是一种传参方式,也分为值捕获和引用捕获,下面分别举例:
//值捕获,前提是变量可以被拷贝
void f1()
{
size_t v1 = 24;
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); //j = 24;
}
//引用捕获,必须保证在捕获时该变量是存在的
void f2()
{
size_t v1 = 24;
auto f = [&v1] { return v1; };
v1 = 0;
auto j = f(); //j = 0;
}
神器:隐式捕获
前面我们都是显式地列出我们希望使用的所在函数中定义的局部变量,这样我们要关心很多,我们可以让编译器来帮助我们做这个事情,让它来推断我们要用哪些变量,例如我们可以重写find_if的lambda:
//sz为隐式捕获,值捕获方式,引用捕获只要把=换成&即可
wc = find_if(words.begin(), words.end(), [=](const string &s){return s.size()>sz;})
其实也没什么卵用,你在函数体里面还是要自己写,也就是在你要用很多捕获变量的时候省点力气。
我们还可以混用隐式捕获和显式捕获,如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获:
//捕获列表的第一个元素必须是=或&,用来指定默认捕获方式为值或引用
[=, &os](const string &s) {os << s << endl;}
可变lambda
==接下来就开始各种搞事情了。。。==
我们知道,对于一个值拷贝的变量,lambda不会改变它的值,本来这样规定就合情合理,但C++说我们也可以改变这个值,在函数参数列表后加上mutable关键字即可:
void fcn2()
{
size_t v1 = 24;
//f可以改变它所捕获的变量的值,即便是值捕获
auto f = [v1]()mutable{return ++v1;}
v1 = 0;
auto j = f(); //j=25
}
对于引用捕获来说,它可不可以修改就取决于它绑定的那个变量是不是const的。
指定lambda返回类型
到目前为止,我们所编写的lambda都只有一个return语句,所以我们还没指定过返回类型,这里有个很奇特的设定:默认情况下,如果一个lambda函数体包含return之外的任何语句,则编译器假定此lambda返回void。我们来举个例子,把容器中所有的负数转正:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i){ return i < 0 ? -i : i; });
我们再来个看起来跟上面差不多的错误版本:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i)
{
if(i<0){return -i;}
else{return i;}
});
报错,因为编译器推断该lambda返回类型为void,但它返回了int。 我们可以通过指定lambda返回类型来修正它(之前我们一直都忽略了返回类型):
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int //通过尾置指定返回类型
{
if(i<0){return -i;}
else{return i;}
});
标准库bind函数来替换lambda
- 只在一两个地方使用的简单操作,用lambda最好
- 很多地方都要用,最好定义一个函数 之前我们怎么引出lambda还记得吗?find_if函数只接受一元谓词作为第三个参数,而我们要给它传一个sz和一个string,所以没办法,于是我们用lambda,因为它可以捕获sz,所以只需要传一个string就好。
接下来我们来介绍其他解决这个问题的办法:
这里比较费解的就是bind函数,我们可以把bind函数看成是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应参数列表,用人话说就是,我这个bind调用了check_size这个函数(可调用对象),它还是接受那个string的参数,但是它把第二个参数绑定到sz上了,这样不就和lambda差不多意思了嘛。using std::placeholder::_1 bool check_size(const string &s, string::size_type sz) { return s.size() >= sz; } auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz)) //_1为占用符
bind的参数
前面的代码说明,我们可以用bind函数来修正参数的值,更一般的,我们还可以用bind绑定给定调用对象中的参数或重新安排顺序:
//假设g是一个有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, _1);
//_1_2才是可调用对象g的参数,其他三个是f的参数,已经绑定好了
//g(_1, _2)映射为f(a, b, _2, c, _1)
调用g(X, Y)会调用f(a, b, Y, c, X)
用bind重排参数顺序
sort(wors.begin(), word.end(), isShorter); //短到长
sort(wors.begin(), word.end(), bind(isShorter, _2, _1); //长到短
默认情况下,bind函数的占位符的参数是会被拷贝到bind返回的可调用对象中,当然这就有问题了,比如有些参数是无法被拷贝的,下面来举个例子(还是用bind函数来替换lambda表达式):
//先来写lambda表达式
for_each(wors.begin(), wors.end(), [&os, c](const string &s){os << s << c;});
//os是一个局部变量,引用一个输出流,c是局部变量char
我们可以编写一个函数来完成同样的工作:
ostream &print(ostream &os, const string &s, char c)
{
return os << s << c;
}
但是,我们不能直接用bind替换,因为os不能被拷贝:
//错误示范
for_each(wors.begin(), wors.end(), bind(print, os, _1, ' '));
我们还是有解决办法的,如果我们希望传递给bind一个对象但是又不拷贝它,就必须使用标准库ref函数:
for_each(wors.begin(), wors.end(), bind(print, ref(os), _1, ' '));
ref和bind函数都定义在头文件functional中。
#C++工程师#