10.3定制操作
- 很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作
- 来代替默认运算符。
- 例如,sort算法默认使用元素类型的<运算符。但可能我们希望的排序顺序与<所定义的顺序不同,或是我们的序列可能保存的是未定义<运算符的元素类型(如Sales_data)在这两种情况下,都需要重载sort的默认行为。
10.3.1向算法传递函数
- 作为一个例子,假定希望在调用elimDups(参见10.2.3节,第343页)后打印vector的内容。此外还假定希望单词按其长度排序,大小相同的再按字典序排列。为了按长度重排vector,我们将使用sort的第二个版本,此版本是重载过的,它接受第三个参数,此参数是一个谓词(predicate)。
谓词
- 谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unarypredicate,意味着它们只接受单一参数)和二元谓词(意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
- 接受一个二元谓词参数的sort版本用这个谓词代替〈来比较元素。我们提供给sort的谓词必须满足将在11.2.2节(第378页)中所介绍的条件。当前,我们只需知道,此操作必须在输入序列中所有可能的元素值上定义一个一致的序。我们在6.2.2节(第189页)中定义的isShorter就是一个满足这些要求的函数,因此可以将isShorter传递给sorta这样做会将元素按大小重新排序:/比较函数,用来按长度排序单词
/ / 比较函数,用来按长度排序单词
bool isShorter(const string &sl, const string &s2){return si.size () < s2.size ();
}
/ / 按长度由短至长排序words
sort(words.begin(), words.end(), isShorter);
- 如果words包含的数据与10.2.3节 (第 343页)中~ 样,此调用会将words重排,使得 所有长度为3的单词排在长度为4 的单词之前,然后是长度为5的单词,依此类推
排序算法
- 在我们将words按大小重排的同时,还希望具有相同长度的元素按字典序排列。为 了保持相同长度的单词按字典序排列,可以使用stable_sort算法。这种稳定排序算法维持相等元素的原有顺序。
- 通常情况下,我们不关心有序序列中相等元素的相对顺序,它们毕竟是相等的。但是,在本例中,我们定义的“相等”关系表示“具有相同长度”。而具有相同长度的元素,如果看其内容,其实还是各不相同的。通过调用stable_sort,可以保持等长元素间的字典序:
10.3.2 lambda 表达式
- 根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。例如,为上一节最后一个练习所编写的程序中,就必须将大小5硬编码到划分序列的谓词中。如果在编写划分序列的谓词时,可以不必为每个可能的大小都编写一个独立的谓词,显然更有实际价值。
- 一个相关的例子是,我们将修改10.3.1节 (第 345页)中的程序,求大于等于一个给定长度的单词有多少。我们还会修改输出,使程序只打印大于等于给定长度的单词。
- 我们的新问题是在v ec to r中寻找第一个大于等于给定长度的元素。一旦找到了这个 元素,根据其位置,就可以计算出有多少元素的长度大于等于给定值。
- 我们可以使用标准库fin d _ if算法来查找第一个具有特定大小的元素。类 似 find (参 见 10.1节,第 336页),fin d _ if算法接受一对迭代器,表示一个范围。但 与 find 不同的是,fin d _ if的第三个参数是一个谓词。fin d _ if算法对输入序列中的每个元素 调用给定的这个谓词。它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。
- 编写一个函数,令其接受一个 string 和一个长度,并返回一个b o o l值表示该 s tr in g 的长度是否大于给定长度,是一件很容易的事情。但是,find_if接受一元谓词,我们传递给find_if的任何函数都必须严格接受一个参数,以便能用来自输入序列 的一个元素调用它。没有任何办法能传递给它第二个参数来表示长度。为了解决此问题,需要使用另外一些语言特性。
介绍lambda
- 我们可以向一个算法传递任何类别的可调用对象(callable object)<>对于一个对象或一个表达式,如果可以对其使用调用运算符(参见1.5.2节,第21页),则称它为可调用
的。即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。 - 到目前为止,我们使用过的仅有的两种可调用对象是函数和函数指针(参见6.7节,第221页)。还有其他两种可调用对象:重载了函数调用运算符的类,我们将在14.8节(第506页)介绍,以及lambda表达式(lambdaexpression)。
- 一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式
- [capture list](parameter list)->return type{function body}其中,capturelist(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用尾置返回(参见6.3.3节,第206页)来指定返回类型。
- 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体 auto f=[] {return 42;};
- 此例中,我们定义了一个可调用对象f , 它不接受参数,返回42。 lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符:cout « f () « endl; // 打印 42
- 在 lambda中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用f 时,参数列表是空的。如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。 如果函数体只是一个return 语句,则返回类型从返回的表达式的类型推断而来。否则, 返回类型为void
- 如果lambda的函数体包含任何单一 return 语句之外的内容,且未指定返回 类型,则返回void
向lambda传递参数
- 与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数(参见 6.5.1节,第 211页)。因此,一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。
- 作为一个带参数的lambda的例子,我们可以编写一个与isShorter函数完成相同功能的lambda:
- [] (const string &a ,const string &b){ return a.size() < b.size(); }
- 空捕获列表表明此lambda不使用它所在函数中的任何局部变量。lambda的参数与isShorter的参数类似,是const string的引用。lambda的函数体也与isShorter类似,比较其两个参数的size(),并根据两者的相对大小返回一个布尔值
使用捕获列表
- 我们现在巳经准备好解决原来的问题了—— 编写一个可以传递给find _ if的可调用 表达式。我们希望这个表达式能将输入序列中每个string 的长度与biggies函数中的 sz参数的值进行比较。 虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。 捕获列表指引lambda在其内部包含访问局部变量所需的信息。
- 在本例中,我们的lambda会捕获sz , 并只有单一的string参数。其函数体会将string的大小与捕获的sz的值进行比较:
- [sz](const string &a) { return a.size () >= sz; };
- lambda以一对[]开始,我们可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的。
- 由于此lambda捕 获 sz , 因此lambda的函数体可以使用sz 。lambda不捕获words,因此不能访问此变量。如果我们给lambda提供一个空捕获列表,则代码会编译错误:
- [](const string &a) ( return a.size() >= sz; }; / / 错误:SZ未捕获 只有前面的[]里面列举出来,后面的{} 里面才可以使用
- 一 个 lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量
调 用find_if
- 使用此lambda,我们就可以查找第一个长度大于等于s z 的元素:
- auto wc = find_if(words.begin(), words.end(), [sz] (const string &a) {return a.size() >= sz; }); / / 获取一个迭代器,指向第一个满足size()>= sz的元素
- 这里对find _ if的调用返回一个迭代器,指向第一个长度不小于给定参数sz的元素。 如果这样的元素不存在,则返回words.end()的一个拷贝。 我们可以使用fin d _ if返回的迭代器来计算从它开始到words的末尾一共有多少个元素 (参 见 3.4.2节,第 99页):
- / / 计算满足size >= sz的元素的数目
- auto count = words.end() - wc; cout « count << " " << make_plural(count, "word", "s") « " of length " << sz « '* or longer" « endl;我们的输出语句调用m ake_plural(参见6.3.2节,第201页)来输出"w ord”或"w ords",具体输出哪个取决于大小是否等于1。
for_each 算法
- 问题的最后一部分是打印words中长度大于等于sz 的元素。为了达到这一目的,我们可以使用for_each算法。此算法接受一个可调用对象,并对输入序列中每个元素调用此对象:
- / / 打卬长度大于等于给定值的单词,每个单词后面接一个空格
- for_each(wc, words.end(), [](const string &s)(cout « s « " ";}); cout « endl;
- 此 lambda中的捕获列表为空,但其函数体中还是使用了两个名字:s 和 c o u t,前者是它自己的参数。
- 捕获列表为空,是因为我们只对lambda所在函数中定义的(非s ta tic ) 变量使用捕获列表。一个lambda可以直接使用定义在当前函数之外的名字。在本例中,cout不是定义在biggies中的局部名字,而是定义在头文件iostream 中。因此,只要在b iggies 出现的作用域中包含了头文件io s tre a m ,我们的lambda就可以使用cout。
- 列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字
#include <iostream>
#include <cstdio>#include <memory>
#include <vector>
#include <algorithm>void elimDups(std::vector<std::string> &words){//按字典顺序排序sort(words.begin(),words.end());//将words进行字典排序,删除重复的单词auto end_unique = unique(words.begin(),words.end());//end_unique指向不重复元素区间的后一位//https://www.cplusplus.com/reference/algorithm/unique///https://www.jianshu.com/p/b8987c8d80b8words.erase(end_unique,words.end());//将多余重复的元素删除
}std::string make_plural(size_t ctr, const std::string &word, const std::string &ending = "s")
{return (ctr > 1) ? word + ending : word;
}void biggies(std::vector<std::string>&words,std::vector<std::string>::size_type sz){elimDups(words);//将words按照字典排序并且删除重复的单词//按照长度排序 长度相同的单词维持字典序std::stable_sort(words.begin(),words.end(),[](const std::string &a,const std::string &b){ return a.size() < b.size(); });//获取一个迭代器 指向第一个满足size() >= sz 的元素auto wc = std::find_if(words.begin(),words.end(),[sz](const std::string &a){ return a.size() > sz; });//计算满足size >= sz 的元素的数目auto count = words.end() - wc;std::cout << count << " " << make_plural(count, "word", "s")<< " of length " << sz << " or longer" <<std::endl;//打印长度大于等于给定数值的单词 每个单词的后面接入一个空格std::for_each(wc,words.end(),[](const std::string &s){std::cout << s << " ";});std::cout<<" "<< std::endl;}using namespace std;
int main(){std::vector<std::string>words {"Hi","Hello","one","two","Hi","three","one"};std::vector<std::string>::size_type sz = 2;biggies(words,sz);
}
10.3.3 lambda捕获和返回
- 当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。 我们将在14.8.1节 (第 507页)介绍这种类是如何生成的。目前,可以这样理解,当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用a u to 定义一个用lambda初始 化的变量时,定义了一个从lambda生成的类型的对象。 默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。 类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化
值捕获
- 类似参数传递,变量的捕获方式也可以是值或引用。表 10.1 (第352页)列出了几种不同的构造捕获列表的方式。到目前为止,我们的lambda采用值捕获的方式。与传值参 数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda 创建时拷贝,而不是调用时拷贝:
- 由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值
引用捕获
- 我们定义lambda时可以采用引用方式捕获变量。例如
- V l之前的&指出v l应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当lambda返回v l 时,它返回的是v l 指向的对象的值。 引用捕获与返回引用(参见6.3.2节,第 201页)有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。 lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
- 引用捕获有时是必要的。例如,我们可能希望biggies 函数接受一个ostream的引用,用来输出数据,并接受一个字符作为分隔符:
void biggies1(std::vector<std::string>&words,std::vector<std::string>::size_type sz,std::ostream &os = std::cout,char c = '\n'){//和之前一样的重排words的代码//打印count的语句改为打印到osstd::for_each(words.begin(),words.end(),[&os,c](const std::string &s){os << s << c;});
}using namespace std;
int main(){std::vector<std::string>words {"Hi","Hello","one","two","Hi","three","one"};std::vector<std::string>::size_type sz = 2;biggies1(words,sz);
}
- 我们不能拷贝ostream 对象 (参 见 8.1.1节,第 279页),因此捕获os 的唯一方法就是捕获其引用(或指向os 的指针)。 当我们向一个函数传递一个lambda时,就像本例中调用for_each那样 lambda 会立即执行。在此情况下,以引用方式捕获os 没有问题,因为当for_each执行时, biggies 中的变量是存在的。 我们也可以从一个函数返回lambd.函数可以直接返回一个可调用对象,或者返回一 个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此 lambda也不能包含引用捕获。
- 当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的
建议: 尽量保持]ambda 的变量捕获简单化
- 一个lambda捕获从lambda被创建(即,定义lambda的代码执行时) 到 lambda自身执行(可能有多次执行)这段时间内保存的相关信息。确保lambda每次执行的时候 这些信息都有预期的意义,是程序员的责任。
- 捕获一个普通变量,如 int、string 或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。
- 如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值,
在 lambda从创建到它执行的这段时间内,可能有代码改变绑定的对象的值。也就是说, 在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在 lambda执行时,该对象的值可能已经完全不同了。 - 一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。
隐式捕获
- 除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda 体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。例如,我们可以重写传递给find _ if的lambda:
- 当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值。当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。
指定 lambda返回类型
- 到目前为止,我们所编写的lambda都只包含单一的return 语句。因此,我们还未遇到必须指定返回类型的情况。默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其他返回void 的函数类似,被推断返回void 的lambda不能返回值。
- 下面给出了一个简单的例子,我们可以使用标准库transform 算法和一个 lambda 来将一个序列中的每个负数替换为其绝对值:
- 函数transform 接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三 个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform 将输入序列中每个元素替换为可调用对象操作该元素得到的结果。
- 在本例中,我们传递给transform 一个 lambda,它返回其参数的绝对值。lambda 体是单一的return语句,返回一个条件表达式的结果。我们无须指定返回类型,因为可以根据条件运算符的类型推断出来。
- 但是,如果我们将程序改写为看起来是等价的if 语句,就会产生编译错误:返回的类型和编译推导出来的类型不一致
10.3.4参数绑定
- 对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要 在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。
- 如果 lambda的捕获列表为空,通常可以用函数来代替它。如前面章节所示,既可以用一个lambda,也可以用函数isShorter来实现将vector中的单词按长度排序。类似的,对于打印vector内容的lambda,编写一个函数来替换它也是很容易的事情,这个函数只需接受一个string并在标准输出上打印它即可。 但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如,我们用在find _ if调用中的lambda比较一个string和一个给定大小。我们可以很容易地编写一个完成同样工作的函数:
- 但是,我们不能用这个函数作为find_if的一个参数。如前文所示, 接受一个 —元谓词,因此传递给find_if的可调用对象必须接受单一参数。biggies传递给find_if的 lambda使用捕获列表来保存sz。为了用check_size来代替此lambda,必须解决如何向sz 形参传递一个参数的问题。
标准库bind函数
- 我们可以解决向check_size传递一个长度参数的问题,方法是使用一个新的名为bind的标准库函数,它定义在头文件functional中。可以将bind函数看作一个通用的函数适配器(参见9.6节,第 329页),它接受一个可调用对象,生成一个新的可调用对象来"适应”原对象的参数列表。
- 调用bind的一般形式为: auto newCallable = bind (callable, arg_list);
- 其中,newCallable本身是一个可调用对象,arg list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传 递给它arg list中的参数。
- argjist中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的"位置”。数值n表示生成的可调用对象中参数的位置:_1 newCallable的第一个参数,_2为第二个参数,依此类推。
绑定check_size的 sz参数
- 作为一个简单的例子,我们将使用bind生成一个调用check_size的对象,如下 所示,它用一个定值作为其大小参数来调用check_size:
- // check6是一个可调用对象,接受一个string类型的参数,并用此string和值6来调用check_size
- auto check6 = bind(check_size, _1, 6);
- 此 bind调用只有一个占位符,表示check6只接受单一参数。占位符出现在arg list的 第一个位置,表 示 check6的此参数对应check_size的第一个参数。此参数是一个 const string&o因此,调用check6必须传递给它一*个 string类型的参数,check6 会将此参数传递给check_sizeo
- string s = "hello"; bool bl = check6 (s) ; // check6 (s)会调用 check_size (s, 6)
- 使用bind,我们可以将原来基于lambda的 f ind_if调用:
- auto wc = find_if(words.begin(), words.end(), [sz] (const string &a)
- 替换为如下使用check_size的版本:
- auto wc = find_if(words.begin(), words.end()r bind(check_size, _1, sz));
- 此 bind调用生成一个可调用对象,将 check_size的第二个参数绑定到sz 的值。当 find_if对 words中的string调用这个对象时,这些对象会调用check_size,将给定的string和 sz 传递给它。因此, 可以有效地对输入序列中每个string 调用check_size,实现string的大小与sz 的比较。
使用 placeholders名字
- 名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在 std命名空间(参见3.1节,第 74页 )中 。为了使用这些名字,两个命名空间都要写 上。与我们的其他例子类似,对 bind的调用代码假定之前己经恰当地使用了 using声明。例如, 对应的using声明为:
- using std::placeholders::_1;
- 此声明说明我们要使用的名字_1 定义在命名空间placeholders中,而此命名空间又定义在命名空间std中。 对每个占位符名字,我们都必须提供一个单独的using声明。编写这样的声明很烦人,也很容易出错。可以使用另外一种不同形式的using语句(详细内容将在18.2.2节 (第702页)中介绍),而不是分别声明每个占位符,如下所示:
- using namespace namespace_name; 例如 using namespace std;
- 这种形式说明希望所有来自namespace_name 的名字都可以在我们的程序中直接使用。例 如:
- using namespace std::placeholders;
- 使得由placeholders定义的所有名字都可用。与 bind函数一样,placeholders命名空间也定义在functional头文件中。
bind的参数
- 如前文所述,我们可以用bind修正参数的值。更一般的,可以用bind绑定给定可调用对象中的参数或重新安排其顺序。例如,假定f 是一个可调用对象,它有5 个参数, 则下面对bind的调用:
- auto g = bind(f, a, b, _2, c, _1); // g 是一个有两个参数的可调用对象
- 生成一个新的可调用对象,它有两个参数,分别用占位符_2和_1表示。这个新的可调用对象将它自己的参数作为第三个和第五个参数传递给f。f 的第一个、第二个和第四个参数分别被绑定到给定的值a、b 和 c。传递给g 的参数按位置绑定到占位符。即,第一个参数绑定到一1,第二个参数绑定到 _2。因此,当我们调用g 时,其第一个参数将被传递给f 作为最后一个参数,第二个参 数将被传递给f 作为第三个参数。实际上,这个bind调用会将g(_l, _2)映射为f (a, b, _2, c, _1)。即,对 g 的调用会调用f,用 g 的参数代替占位符,再加上绑定的参数a、b 和 c。例如, 调用g(x,Y)会调用f (a, b, Y, c, X)
用 bind重排参数顺序
- 下面是用bind重排参数顺序的一个具体例子,我们可以用bind颠倒 isShroter 的含义:
- sort(words.begin(), words.end(), isShorter); / / 按单词长度由短至长排序
- sort(words.begin(), words.end(), bind(isShorter, _2, _1));/ / 按单词长度由长至短排序
- 在第一个调用中,当 sort需要比较两个元素A 和 B 时,它会调用isShorter (A, B) ,在第二个对sort的调用中,传递给isShorter的参数被交换过来了。因此,当 sort 比较两个元素时,就好像调用isShorter (B,A)-样。
绑定引用参数
- 默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。 但是,与 lambda类似,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定参数的类型无法拷贝。例如,为了替换一个引用方式捕获ostream的 lambda:
- for_each(words.begin(), words.end(), [&os, c] (const string &s) ( os << s « c; }); // os是一个局部变量,引用一个输出流 ,c 是一个局部变量,类型为char