很多算法都会比较输入序列中的元素,默认情况下,这类算法使用元素类型的==或者<运算符来完成比较。标准库为这些算法定义了额外的版本,允许我们提供自己定义的操作来完成默认运算符。
1、向算法传递函数
谓词:是一个可以调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(他们只能够接受单一参数)二元谓词(着他们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。
元素类型必须能够转换为谓词的参数类型。
代码示例:
bool isShorter (const string &s1,const string &s2)
{
retrun s1.size()<s2.size();
}
sort(words.begin(),words.end(),isShorter);
示例算法:stable_sort
原理:稳定排序算法维持相等元素的原有顺序。可以保持等长元素的字典序。
2、lambda表达式
问题:根据算法接受一元谓词还是二元谓词。传递给算法的谓词必须严格接受一个或者两个参数。但是希望进行的操作需要更多参数,超出了算法对谓词的限制。
比如find_if泛型算法查找第一个具有特定大小的元素。第三个参数是一个一元谓词。算法对输入序列当中的每一个元素都调用这个谓词。他返回第一个使谓词返回非零值的元素,如果不存在这样的元素,那么返回尾迭代器。
解决方法:可以向一个算法传递任何类别的可调用对象。对于一个对象或者一个表达式,如果可以对其使用调用运算符(调用运算符是一对圆括号),则称它为可调用的。
可调用对象:函数,函数指针,重载了函数调用运算符的类,lambda表达式。
定义:lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,具有一个返回类型、一个参数列表和一个函数体。但是和函数不同,lambda可能定义在函数的内部。
具体形式:
[capture list](parameter list)->return type{function body}
具体含义:
capture list(捕获列表):是一个lambda所在函数中定义的局部变量的列表(通常为空)。
return type:返回值类型。
parameter list:参数列表。
function body:函数体。
lambda必须使用尾置返回,来指定返回类型。
尾置返回类型:任何函数的定义都能使用尾置返回。对返回类型复杂的非常有效。尾置返回类型跟在形参列表后面以一个->符号开头,为了表示函数真正的发挥类型跟在形参列表之后,我们在本应该出现返回类型的地方放置auto。
auto func(int i)->int(*)[10];
注意:我们可以忽略参数列表和返回类型,但是必须永远包含捕获列表和函数体。忽略括号和参数列表等价于制定一个空参数列表。忽略返回类型,lambda表达式根据函数体中的代码推断出返回类型。
auto f=[]{return 42;}
cout<<f()<<endl;
向lambda传递参数:调用一个lambda时,给定的实参被用来初始化lambda的形参。实参和形参的类型必须匹配。lambda不能有默认参数。一个lambda调用的实参数目永远和形参数目相等。一旦形参初始化完毕,就可以执行函数体了。
示例代码:
[](const string &a,const string &b)
{return a.size()<b.size();}
使用捕获列表:一个lambda只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引lambda在其内部访问局部变量所需的信息。
[sz](const string &a){return a.size()>=sz;}
//可以提供一个以逗号分隔的名字列表,这些名字都是所在函数定义的。
一个lambda只有再起捕获列表中捕获一个他所在函数中的局部变量,才能够在函数体中使用该变量。
示例程序:
auto wc=find_if(words.begin(),words.end(),[sz](const string &a){return a.size()>=sz;});
//返回一个迭代器,指向第一个长度不小于给定参数sz的元素。
for_each(wc,words.end(),[](const string &s){cout<<s<<" ";});
//for_each算法:接受一个可调用对象,并对输入序列中的每个元素调用这个对象。一个lambda可以直接使用定义在当前函数之外的名字。
cout<<endl;
捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。
3、lambda捕获和返回
当向一个函数传递一个lambda的 时候,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。例如使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
(1)值捕获
类似参数传递,变量的捕获方式也可以是值或者引用。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
示例程序:
void func()
{
size_t v1=42;//局部变量
auto f=[v1]{return v1;}
v1=0;
auto j=f();//j为42,f保存了创建之时的v1拷贝
}
(2)引用捕获
可以采用引用方式进行捕获变量。
void func()
{
size_t v1=42;
auto f2=[&v1]{return v1;}
v1=0;
auto j=f2();//j为0,f2保存了v1的引用,而非拷贝。
}
//在lambda函数体内使用此变量的时候,实际上使用的是引用所绑定的对象。
如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候存在。比如:lambda捕获的都是局部变量,这些变量在函数结束后就不复存在。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
注意:不能拷贝ostream对象,捕获的唯一方法是捕获其引用。
关于函数返回lambda的问题:函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。
尽量保持lambda的变量捕获简单化:一个lambda捕获从lambda被创建(定义lambda的代码执行时)到lambda自身执行这段时间内保存相关信息,确保lambda每次执行的时候,这些信息都有预期的意义。捕获简单变量如int、string等非指针类型,利用值捕获来完成。捕获指针或者迭代器,采用引用捕获的方式。
(3)隐式捕获
可以让编译器根据lambda体重的代码来推断我们要使用哪些变量。为了只是编译器推断捕获类表。应该在捕获列表中写一个&或者=。&:采用捕获引用的方式,=表示采用值捕获的方式。
当我们混合使用隐式和显式捕获时,捕获列表中的第一个元素必须是一个&或者=。
混合使用隐式捕获和显式捕获的时候,显式捕获的变量必须使用隐式捕获不同的方式。如果隐式捕获时&,显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。
捕获列表:
[]:空捕获列表。lambda不能使用所在函数中的变量。
[names]:names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。
[&]:隐式捕获列表,引用方式。
[=]:隐式捕获列表,值捕获方式。
[&,identifier_list]:identifier_list是一个逗号分隔的列表,包含0个或者多个来自所在函数的变量。变量采用值捕获方式。任何隐式捕获的变量都采用引用方式捕获。
[=,identifier_list]:identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list不能包括this,且这些名字之前必须用&。
示例程序:
void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os=cout,char c='')
{
for_each(words.begin(),words.end(),[&,c](const string &s){os<<s<<c;});
for_each(words.begin(),words.end(),[=,&os](const string &s){os<<s<<c;})
}
(4)可变lambda
对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能够改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能够省略参数列表。
示例程序:
void func()
{
size_t v1=42;
//局部变量。
auto f=[v1]()mutable{return ++v1;};
v1=0;
auto j=f();//j为43.
}
一个引用捕获的变量是否可以修改依赖于此引用指向的是一个const类型还是一个非const类型。
void func()
{
size_t v1=42;
//局部变量。
auto f=[&v1]{return ++v1;};
v1=0;
auto j=f();//j为1.
}
(5)指定lambda返回类型
默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。
如果我们要为一个lambda定义返回类型的时候必须使用尾置返回类型。
示例程序:
transform(v.begin(),v.end(),v.begin(),
[](int i)->int{if(i<0)retun -i;else return i;});
4、参数绑定
对于lambda使用原则:只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。
如果一个lambda表达式的捕获列表为空,那么很容易使用一个函数对其进行替换。
如果lambda捕获局部变量,而且受到算法关于谓词的限制,就不太容易使用函数对其进行替换。
(1)标准库bind函数
头文件:functional
原理:可以将bind看做函数适配器,接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
auto newCallable = bind(callable,arg_list);
解释:newCallable本身是一个可调用对象。arg_list是一个逗号分隔的参数列表,对应给定的callable参数。当我们调用newCallable的时候,会调用Callable并且传递给它arg _list中参数。
注意:arg_list中的参数可能包含形如 _n的名字。n为一个整数,这些参数名为“占位符”,表示newCallable的参数,他们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置。
示例代码:
auto check=bind(check_size,_1,6);
//这个bind调用只有一个占位符,表示check只接受单一参数。占位符出现在arg_list的第一个位置,表示check的此参数对应check_size的第一个参数。此参数是const string&。
//原来的find_if算法基于lambda。
auto wc=find_if(words.begin(),words.end(),[sz](const string &a)){return a.size()<sz};
//基于bind的find_if算法
auto wc=find_if(words.begin(),words.end(),bind(check_size,_1,sz));
//将check_size的第二个参数绑定到sz的值。
bind参数
可以用bind绑定给定可调用对象中的参数或重新安排其顺序。
auto g=bind(f,a,b,_2,c,_1);
上述代码生成一个新的可调用对象。有两个参数,分别用占位符_2, _1表示。传递给g的参数按照位置绑定到占位符上。第一个参数绑定到 _1,第二个参数绑定到 _2。
利用bind重排参数顺序。
sort(words.begin(),words.end(),bind(isShorter,_2,_1));
利用bind绑定引用参数
bind拷贝其参数,但是不能拷贝像ostream这样的对象。传递一个引用的方法:ref。
ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref,生成一个保存const引用的类。(定义在functional头文件中)
(2)使用placeholders名字
名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std的命名空间中。为了使用这些名字,两个命名空间都要写上。
using namespace std::placeholders;
上述代码使得placeholders当中定义的所有名字都可用。与bind函数一样,placeholders命名空间也定义在functional头文件中。
本文详细介绍了C++中lambda表达式的概念、应用实例、捕获列表的使用、返回类型设定以及参数绑定等核心内容,旨在帮助开发者高效地在代码中运用lambda表达式。
1437

被折叠的 条评论
为什么被折叠?



