10-3 定制操作lambda
10.3.1 向算法传递函数
作为一个例子,假定希望在调用elimDups(参见10.2.3节)后打印vector的内容。
此外还假定希望单词按其长度排序,大小相同的再按字典序排列。
为了按长度重排vector,我们将使用sort 的第二个版本,此版本是重载过的,它接受第三个参数,此参数是一个谓词(predicate)。
谓词#
- 谓词接受一个参数【一元谓词】或两个参数【二元谓词】,在所有的可能值上定义了一个一致的序
- sort函数会用谓词函数代替“<”来对元素排序
//比较函数,按长度排序
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}
//由短到长排序words
sort(words.begin(), words.end(), isShorter);
排序算法#
在我们将 words按大小重排的同时,还希望具有相同长度的元素按字典序排列。
为了保持相同长度的单词按字典序排列,可以使用stable_sort算法。这种稳定排序算法维持相等元素的原有顺序。
elimDups(words); //将words按字典序重排,消除重复单词
//按长度重排,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
//源程序
#include<iostream>
#include<vector>
#include<algorithm>
#include<iterator>
using namespace std;
void elimDups(vector<string> &words){
//按字典排序words,以便查找重复单词
sort(words.begin(), words.end());
//unique重排输入范围,使得每个单词只出现一次
//排列在范围前部,返回指向不重复区域之后一个位置的迭代器
auto end_unique = unique(words.begin(), words.end());
//使用earase删除重复单词
words.erase(end_unique, words.end());
}
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}
int main(){
vector<string> words = {"slow","the","fox","jumps","over","quick",
"red","the","turtle"};
elimDups(words);
stable_sort(words.begin(), words.end(), isShorter);
for(string s : words)
cout<<s<<" ";
cout<<endl;
return 0;
}
//输出:fox red the over slow jumps quick turtle
10.3.2 lambda表达式
引入#
求words中大于一个给定长度的单词有多少个,并打印满足条件的单词
将次函数命名为“biggies”,框架如下:
现在的新问题是在vector中寻找第一个大于等于给定长度的元素。
我们可以使用标准库find_if 算法来查找第一个具有特定大小的元素。
类似find(参见10.1节,第336页), find_if 算法接受一对迭代器,表示一个范围。
但与 find不同的是,find_if 的第三个参数是一个一元谓词。
find_if算法对输入序列中的每个元素调用给定的这个谓词。它返回第一个使谓词返回非О值的元素,如果不存在这样的元素,则返回尾迭代器。
编写一个函数,令其接受一个string 和一个长度,并返回一个bool值表示该string的长度是否大于给定长度,是一件很容易的事情。
但是,find_if接受一元谓词——我们传递给find_if 的任何函数都必须严格接受一个参数,以便能用来自输入序列的一个元素调用它。但我们又不得不给它传入第二个参数来表示长度。
为了解决此问题,需要使用另外一些语言特性。
介绍lambda#
lambda表达式是一个可调用对象
可调用对象(callable object):
对于任何一个对象或者表达式,只要可以对它使用调用运算符“()”,我们就称该对象或表达式是“可调用的”
常见的可调用对象
- 函数和函数指针
- 重载了函数调用运算符的类(14.8节介绍)
- lambda表达式
lambda表达式可以理解为一个未命名的内联函数,可以定义在函数中
构成:
-
capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);
-
return type、parameter list和 function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。
-
但是,与普通函数不同,lambda必须使用尾置返回(参见6.3.3节,第206页)来指定返回类型。
可以忽略参数列表和返回类型,但必须包含捕获列表和函数体
//定义了一个lambda:不接受任何参数,返回42
auto f = []{return 42;};
//调用方式和函数相同,用调用运算符
cout<<f()<<endl; //打印42
当函数体只有一个return语句时,lambda表达式可以推断出返回类型
如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
向lambda传递参数#
lambda不接受默认实参
传递参数举例:
[](const string &a, const string &b)
{return a.size() < b.size();}
- “[]”:不需要所在函数中的任何局部变量
- “(…)”:参数
- 只有return语句,忽略返回类型
- “{…}”:函数体
//按长度排序,长度相同维持字典序
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{return a.size()<b.size();});
使用捕获列表#
lambda表达式可以通过捕获列表,使用函数中定义的局部变量
biggies函数中有一个局部变量sz,可以定义如下lambda表达式来把参数的大小与sz比较
[sz](const string &a) {return a.sise() >= sz;}
一个 lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在lambda的函数体中使用该变量。【有点绕,具体看例子】
调用find_if#
现在可以写出find_if【在biggies函数内使用,biggies内定义了sz】了
//获取迭代器,指向第一个size>=sz的元素
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{return a.size() >= sz;});
使用for_each#
for_each(迭代器范围, 可调用对象)
:对迭代器范围内的每个元素调用传入的可调用对象
//打印长度>=给定值的单词,每个单词后面接一个空格
for_each(wc, wc.words.end(),
[](string &s){cout<<s<<" ";});
完整的biggies#
void biggies(vector<string> &words, int sz){
elimDups(words); //words按字典排序,删除重复单词
//按长度排序,长度相同的维持字典序
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{return a.size()<b.size();});
//获取迭代器,指向第一个满足size()>=sz的元素
auto wc = find_if(words.begin(), words.end(),
[sz](const string &s)
{return s.size()>=sz;});
//计算满足size>=sz的元素个数
auto count = words.end() - wc;
cout<<"count = "<<count<<endl;
//打印长度大于给定值的单词
for_each(wc, words.end(),
[](string &s)
{cout<<s<<" ";});
cout<<endl;
}
10.3.3 lambda的捕获和返回
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。我们将在14.8.1节(第507页)介绍这种类是如何生成的。
目前,可以这样理解,当向一个函数传递一个lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从 lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
值捕获#
类似于参数传递,捕获的方式可以是值捕获也可以是引用捕获
采用值捕获的前提是可以拷贝
与函数参数传递不同,lambda表达式捕获的变量的值是在创建时拷贝,而不是调用时拷贝
void fcn1(){
int v1 = 42;
auto f = [v1]{return v1;};
v1 = 0;
cout<<f()<<endl;
//输出为42
}
引用捕获#
void fcn2(){
int v1 = 42;
auto f = [&v1]{return v1;};
v1 = 0;
cout<<f()<<endl;
//输出为0
}
引用捕获是必要的,毕竟有些对象如ostream对象,是无法拷贝的,只能采用引用捕获
采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。
lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
我们也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获,因为捕获的对象是局部对象,在函数结束时已经消失。
建议:尽量保持lambda的变量捕获简单化
一个 lambda捕获从 lambda被创建(即,定义 lambda的代码执行时)到lambda自身执行(可能有多次执行)这段时间内保存的相关信息。确保 lambda每次执行的时候这些信息都有预期的意义,是程序员的责任。
捕获一个普通变量,如int.string或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。
如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在 lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值在 lambda从创建到它执行的这段时间内,可能有代码改变绑定的对象的值。也就是说,在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在 lambda执行时,该对象的值可能已经完全不同了。
一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且.如果可能的话,应该避免捕获指针或引用。
隐式捕获#
除了显式地指定捕获的变量外,可以让编译器根据代码自动推断我们要使用哪些变量,此时应在捕获列表写一个&
或=
-
&
:引用捕获 -
=
:值捕获
我们可以重写find_if
//sz为隐式捕获,值捕获
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{return s.size()>=sz;});
可以混合使用显示捕获和隐式捕获
void print(vector<string> &words, ostream &os, char c = ' '){
//os隐式捕获,引用捕获;c显示捕获,值捕获
for_each(words.begin(), words.end(),
[&, c](const string s){os<<s<<c;});
//os显示捕获,引用捕获;c隐式捕获,值捕获
for_each(words.begin(), words.end(),
[=, &os](const string s){os<<s<<c;});
}
- 混合使用显示捕获和隐式捕获时,第一个参数必须是
&
或=
- 显示捕获和隐式捕获的变量必须采取不同的捕获方式
可变lambda#
默认情况下
-
引用捕获时,可以改变捕获变量的值
-
值捕获时,无法改变捕获变量的值,但可以通过添加
mutable
关键字实现这点
void fcn3(){
int v1 = 42;
//此时参数列表不可省略
auto f = [v1] () mutable {return ++v1;};
vl = 0;
cout<<f()<<endl;
//输出为43
}
指定lambda的返回类型#
-
当lambda函数体中只包含return语句,所以lambda可以自动推断返回类型,返回类型可以省略
-
当lambda中包含其他语句(循环,判断…),就必须指定返回类型,否则默认返回void
下面给出了一个简单的例子
我们可以使用标准库 transform算法和一个lambda来将一个序列中的每个负数替换为其绝对值:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i){return i<0 ? -i : i;});
函数transform接受三个迭代器和一个可调用对象。
前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。
如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform将输入序列中每个元素替换为可调用对象操作该元素得到的结果。
本例子中,lambda中只有return语句,所以无需指定返回类型,如果改写为:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i){if(i<0) return -i; else return i;});
会产生编译错误,应该指定返回类型,写为:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i)->int{if(i<0) return -i; else return i;});
ps.不知道为什么我没有指定返回类型在vscode上也通过编译了,不过vscode编译检查并不严格,为了保证程序的可移植性,最好加上返回类型
int main(){
fcn3();
vector<int> vi = {1,-2,4,-3};
transform(vi.begin(), vi.end(), vi.begin(),
[](int i)->int{if(i<0) return -i; else return i;});
for_each(vi.begin(), vi.end(), [](int i){cout<<i<<" ";});
return 0;
//输出1,2,4,3
}
算法的if版本#
在10.2.2 已经讲过,算法的拷贝版本xxx_copy(指定容器的迭代器,...)
可以将函数的运行结果拷贝到指定的容器中
而算法的if版本xxx_if(...,谓词)
则是对迭代器范围内的元素调用可调用对象
-
find_if(iter1, iter2, 一元谓词):返回第一个使得谓词为真的元素的迭代器
-
cout_if(iter1, iter2, 谓词):返回使得谓词为真的元素的个数
int main(){ vector<int> vi = {1,-2,4,-3,5}; cout<<count_if(vi.begin(), vi.end(), [](int i){return i>0;})<<endl; return 0; //输出为3 }
-
for_each(iter1, iter2, 可调用对象):对范围内的每个元素调用可调用对象
//遍历的新写法 for_each(vi.begin(), vi.end(), [](int i){cout<<i<<" ";});
-
transform(iter1, iter2, iter3, 可调用对象):见上文
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用