谓词(predicate)
标准库中定义了一类可以“定制操作”的算法函数,这类函数接收一个参数,这个参数与以往我们所认识的参数不同,它是一个可调用的对象,其返回结果是一个能用作条件的值。
这个参数就是谓词。 我们通过谓词来定制特定的操作。
谓词分为一元谓词和二元谓词,表示可调用表达式接收一个还是两个参数。
谓词的使用例子:
bool isShorter(const string &s1,const string &s2)
{
return s1.size()<s2.size();
}
sort(words.begin(),words.end(),isShorter());
//isShorter函数就是一个谓词,接受什么样的谓词(一元还是二元)由算法本身决定,且必须严格遵守
//如果希望使用更多的参数,可以使用lambda表达式和标准库函数bind
可调用对象
可调用对象就是可以对其使用调用运算符的对象,有下面四四种:
- 函数
- 函数指针
- 重载了函数调用运算符的类
- lambda表达式
如果希望使用更多的参数,可以使用lambda表达式和标准库函数bind
lambda表达式
lambda表达式是一个可调用的代码单元,可以定义在函数内部,标准形式如下
[capture list](parameter list) -> return type { function body }
- cpature list
捕获列表
捕获并可以使用其所在函数中定义的局部变量
可以为空
//example
int main()
{
int a = 10; //main()中定义的局部变量
auto lbd = [a](int b) ->int {return a+b;};
//捕获了所在函数main()中定义的局部变量 a
cout<<lbd(5)<<endl; //输出15
return 0;
}
- parameter list
参数列表
和函数的参数列表一致
可以为空
int a = 10;
auto f1 = [a](){ return a; };
auto f2 = [a] {return a; }; //省略参数列表,两种形式都可以
- return type
返回类型
必须用尾置类型
可以为空
如果缺省不写,编译器根据return语句返回的类型自行推断
※ 如果lambda函数体中有除了return以外的其他语句,且返回类型未指定,则返回类型为void
// 这个例子在visual studio2017中可以编译并正确运行,可能为编译器的优化
[](int a)
{
++a;
return a; //未指定返回类型,编译器推断为void,实际返回a为int,编译错误
}
- function body
函数体
就是函数体呀
使用捕获
这个问题下有三个重要概念:值、引用捕获 、 隐式捕获 、 可变捕获
值、引用捕获
和参数传递相似,捕获也有值捕获和引用捕获两种方式:
- 值捕获
拷贝值,对拷贝值的修改不会影响原变量
// 值捕获
int v1 = 10;
auto f1 = [v1]()
{
++v1; //不会影响局部变量v1的值
}
- 引用捕获
捕获对象的引用
注意指明引用捕获的写法
//引用捕获
auto f2 = [&v1]() //指明捕获的是v1的引用,不是捕获v1的地址
{ //实际捕获列表只能填局部变量名,或用&指明是引用
//并不像填写参数那样可以用取地址和取值运算符(&、*)
++v1; //真实改变了v1的值
}
隐式捕获
我们可以不用写出捕获哪些变量,而在函数体中直接使用它们,编译器会推断出要捕获哪些变量
但仍有几条规定
- 与空捕获区分
wc = find_if(words.begin(),words.end(),[=](const string &s){ return s.size() >= sz;});
//捕获列表中填写了一个 “=” 告诉编译器我们使用隐式捕获而不是空捕获
// “=” 表示采用值捕获
捕获列表中填写 “=”表示采用值捕获,“&”表示采用引用捕获
如果希望使用混合捕获模式,只需要在捕获列表的第一位填入基本的捕获模式,在第二位填入例外的捕获变量表
//混合捕获
int v1,v2,v3;
auto f = [=,&v3](){
++v1;
++v2;
++v3;
};
//基本捕获模式是值捕获,根据函数体推断出要捕获v1,v2,v3,然后第二位指定对v3采用引用捕获
可变捕获
值捕获一个局部变量后,实际上是拷贝了这个变量的值,函数体中无法对这个值进行修改。
使用mutable后可以修改这个拷贝值(依然不对原变量产生影响)
int a = 10;
auto f1 = [a](){return ++a;}; //error,不可以修改a(拷贝)的值
auto f2 = [a]() mutable{return ++a;}; //正确
a = 999;
cout<<a<<endl
<<f2()<<endl;
//输出 999
// 11
从上面的例子我们可以看出:
- f2中a(拷贝)的值就是捕获时拷贝的变量a的值,这符合值捕获的规律,在lambda定义时捕获
- f2中对a(拷贝)的修改,修改的是拷贝的副本,不会影响原变量
虽然mutable可以修改值捕获的值,但与引用捕获有本质不同,要注意区分
参数绑定 bind
bind是一个标准库函数,定义在头文件functional中
调用bind的一般形式为:
auto newCallable = bind(callable,arg_list);
- newCallable
bind生成一个可调用对象 - callable
bind接受一个可调用对象,不妨暂且理解为一个函数 - arg_list
arg_list就是传递给callable的参数表,个数必须与callable的参数个数一致
arg_list分为两部分,一是占位符,形如_n;二是绑定参数,与newCallable调用时的参数无关,一经声明就与callable绑定
下面一个实例帮助理解
//callable原型,使用两个参数
check_size(const string &s,const int sz)
{
return s.size()<sz;
}
//使用bind的目的便是实现只用一个参数达到相同的目的
//newCallable完整声明
auto newCallable = bind(check_size,_1,sz); //args_list就是 _1,sz
//通过调用newCallable实际调用callable
string A = "hello";
int sz = 5;
bool b1 = newCallable(A); //这句等同于 调用check_size(A,sz),
//newCallable的第一个参数A被填入占位符_1的位置
//bind作为谓词的使用实例
auto wc = find_if(words.begin(),words.end(),bind(check_size,_1,sz));
其中占位符_n定义在名字空间 std::placeholders 中
bind的参数映射
定义一个bind
auto g = bind(function,a,b,_2,c,_1);
把可调用对象function看作一个函数,可以推断出function一定有5个参数
形如
return type function(para1,para2,para3,para4,para5);
bind的args_list中有两个占位符,可以推断g接收两个参数
这样调用g
g(A,B);
然后g的参数按照占位符填入function中
function(a,b,B,c,A);
绑定引用参数
用一个例子说明占位符与绑定参数传值的不同
void Change(int &a,int &b) //两个参数均为传引用
{
++a;
--b;
}
int main()
{
int a = 0;
int b = 0;
auto g = bind(Change, a, _1); //绑定引用参数版本为
//auto g = bind(Change,ref(a),_1);
g(b);
cout << a << endl
<< b << endl;
system("pause");
return 0;
}
绑定非引用参数输出结果
绑定引用参数输出结果
原因在绑定参数进行了两次参数传递,而占位符只进行了一次参数传递
change函数定义a,b参数都通过引用传参
而a是绑定参数,进行了两次传参,第一次(调用g,将绑定参数a传给Change函数)若是值传递,则第二次(从Change参数表传入函数内部,已经定义为引用传参)也只是引用第一次的值拷贝,没有真正引用到最开始的变量
下面一个例子说明这个问题
void Change1(int &a) //传引用
{
++a;
}
void Change2(int a) //传值
{
Change(a); //将值拷贝的引用传入Change1
}
int main()
{
int a = 0;
Change2(a); //调用change2,传入a的值
cout << a << endl; //输出0,a的值并为改变
system("pause");
return 0;
}
如果希望传入const引用,可以使用cref函数
cref与ref都定义在头文件 functional中