参考:c++primer
主要介绍泛型算法中的谓词
向算法传递函数
有些算法接受一个叫做谓词
的参数,所谓谓词
,就是一个可调用的表达式,其返回结果是一个能用作条件的值。
标准库算法使用的谓词分为两类:一元谓词(unary predict)
和二元谓词(binary predict)
,几元就表示该谓词接受几个参数。
举个例子:
vector<string> names;
//要求按string长度对其进行排序
bool cmp(const string &s1, const string &s2){ //一个二元谓词
return s1.size() < s2.size();
}
sort(names.begin(), names.end(), cmp);
有时候上面这种做法是没问题的,不过…
再看这个例子:
find_if
算法第三个参数是一个一元谓词
,它对每个元素调用这个谓词,返回第一个使这个谓词为真的元素迭代器。
如果我们要查找长度大于某一定值的name,就得传入两个参数,但一元谓词不允许!
bool cmp(const string &s, int sz){
return s.size() > sz;
}
find_if(names.begin(), names.end(), cmp); //报错,find_if只接受一元谓词
当然解决方式可以把cmp函数中的sz硬编码为某一数值,只传一个参数就可以了。
不过我们有更好的办法,即lambda表达式。
lambda表达式
先介绍一下lambda的写法:
[捕获列表](参数列表) -> 返回类型 { 函数体 } //使用了尾置返回类型,捕获列表和函数体是必须的,其它都可以省略
所谓捕获列表,就是lambda所在函数中定义的局部变量的列表。
还是上面那个例子,用lambda表达式可以这么写:
int sz = 5;
find_if(names.begin(), names.end(), [sz](const string &s){
return s.size() > sz;
});
还是很好理解的吧~
再举个例子:for_each算法
for_each(names.begin(), names.end(), [](const string &s){
cout<<s<<" ";
});
当定义一个lambda表达式时,编译器生成了一个与lambda对应的新的未命名的类类型。
比如上面find_if中的lambda对应的类类型是这样的:
class cmp{
public:
cmp(int sz): sz(sz){ }
bool operator()(const string &s)const{ //注意这里必须是const,因此不能修改sz
return s.size() > sz;
}
private:
int sz; //数据成员对应捕获列表
};
//调用时这样
find_if(names.begin(), names.end(), cmp(sz));
于是根据上面生成的类类型我们就可以理解可变lambda
是怎么回事:
如果想要修改捕获列表的值,就要用mutable
修饰
int sz = 5;
find_if(names.begin(), names.end(), [sz](const string &s) mutable{
sz++;
sz--; //这里只是为了说明问题
return s.size() > sz;
});
//对应的类如下:
class cmp{
public:
cmp(int sz): sz(sz){ }
bool operator()(const string &s)const{ //虽然是const,但是sz用mutable修饰了,因此可以修改sz
sz++;
sz--;
return s.size() > sz;
}
private:
mutable int sz; //数据成员对应捕获列表
};
当然也可以用引用捕获,不过这样以来捕获列表就是一个别名,能否被修改就取决于该值定义是否是const。
再看一个例子:
int a = 3, b = 3;
auto f = [&a]{
a++;
return a;
};
auto g = [b]()mutable{ //注意这里必须用mutable修饰,而且参数列表那个括号不可以省略
b++;
return b;
};
int ans1 = f(); //ans1 = 4;
int ans2 = g(); //ans2 = 4;
cout<<"ans1 =: "<<ans1<<endl;
cout<<"ans2 =: "<<ans2<<endl;
a = 0; //会影响到f中的a值,二者是同一个对象
b = 0; //g中的b是它的副本,没有直接关系
ans1 = f(); //ans1 = 1;
ans2 = g(); //ans2 = 5;
cout<<"ans1 =: "<<ans1<<endl;
cout<<"ans2 =: "<<ans2<<endl;
看完上面这个例子应该彻底理解了吧
下面说明捕获列表的几种情况:
- 值捕获
- 引用捕获
- 隐式捕获
值捕获
和引用捕获
对应函数中的参数传值和传引用,这里就不多说了,写法上引用捕获前面加个&
符号。
至于隐式捕获
,编译器可以根据函数体中用到的变量推断出捕获列表,自动捕获,不过我们需要告诉编译器是值捕获([=]
)还是引用捕获([&]
),也可混用。
值捕获 | 引用捕获 | 默认引用捕获,指定值捕获 | 默认值捕获,指定引用捕获 |
---|---|---|---|
[=] | [&] | [&, list] | [=, list] |
其中list是逗号分隔的参数列表。
下面再说返回类型:
如果lambda表达式中只有一条返回语句,那么不需要写返回类型,编译器会自动推断,我们上面写的就是这种。
如果除了返回语句之外的其他语句,编译器则默认返回void。
然而实测之后编译器可自动推断返回类型。。。
比如:
vector<int> v{1, 4, -5};
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; //没有像书上说的报错
});
参数绑定
lambda是匿名对象,只用一次的地方和方便,如果要多次使用,那么通常应该定义一个函数,不过对于有捕获列表的lambda怎么用函数来替换呢?用bind。
bind可以看做通用的函数适配器,可接收一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
还是之前找大于某个长度的name的那个例子,让我们看一下用bind怎么实现:
using namespace std::placeholders; //注意_1在这个命名空间中
bool cmp(const string &s, int sz){
return s.size() > sz;
}
int sz = 5;
auto f = bind(cmp, _1, sz); //_1叫做占位符,对应谓词中第一个参数,以此类推;
find_if(names.begin(), names.end(), f);
需要注意bind中不是占位符的参数是拷贝方式传递的,那么如何才能引用的方式传参呢?这对于不能拷贝的对象(比如ostream)来说至关重要。
方法是用ref
和cref
。
ref
返回一个对象,包含给定的引用,cref
生成一个保存const引用的类。
void print(ostream &os, const string &s, string op){
os<<s<<op;
}
for_each(names.begin(), names.end(), bind(print, ref(cout), _1, " - "));
算法命名规范
算法形参模式
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
一些算法使用重载形式传递一个谓词
unique(beg, end);
//使用==比较元素unique(beg, end, comp);
//使用comp比较元素
_if版本的算法
find(beg, end, val);
//查找val第一次出现的位置find_if(beg, end, pred);
//查找第一个另pred为真的元素的位置
区分是否拷贝元素
reverse(beg, end);
//反转自身reverse(beg, end, dest);
//将反转后的拷贝到dest