97.泛型算法

Posted codemagiciant

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了97.泛型算法相关的知识,希望对你有一定的参考价值。

  顺序容器只定义了很少的操作:在多数情况下,我们可以添加和删除元素、访问首尾元素、确定容器是否为空以及获得指向首元素或尾元素之后位置的迭代器。
  用户可能还希望做其他很多有用的操作:查找特定元素、替换或删除一个特定值、重排元素顺序等。
  标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法(generic algorithm):称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索;称它们是“泛型的”,是因为它们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,如vector或list,还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列。

1.概述

  大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。
  一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围(参见9.2.1节,第296页)来进行操作。通常情况下,符法遍历范围,对其中每个元素进行一些处理。例如,假定我们有一个int的vector,希望知道vector中是否包含一个特定值。回答这个问题最方便的方法是调用标准库算法find:

int val = 42;//我们将查找的值
//如果在vec中找到想要的元素,则返回结果指向它
auto result = find(vec.cbegin(), vec.cend(), val); 
//报告结果
cout << "The value"<< val << (result== vec.cend() ? "is not present": " is present") << endl; 

传递给find的前两个参数是表示元素范围的迭代器,第三个参数是一个值。find将范围中每个元素与给定值进行比较。它返回指向第一个等于给定值的元素的迭代器,如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。因此,我们可以通过比较返回值和第二个参数来判断搜索是否成功。我们在输出语句中执行这个检测,其中使用了条件运算符(参见4.7节,第134页)来报告搜索是否成功。
  由于find操作的是迭代器,因此我们可以用同样的find函数在任何容器中查找值。例如,可以用find在一个string的list中查找一个给定值:

string val= "a value";//我们要查找的值
//此调用在list中查找string元素
auto result = find(lst.cbegin(), lst.cend(),val); 

类似的,由于指针就像内置数组上的迭代器一样,我们可以用find在数组中查找值:

int ia[] = 27, 210, 12, 47, 109, 83; 
int val = 83; 
int* result = find(begin(ia), end(ia), val); 

此例中我们使用了标准库begin和end函数(参见3.5.3节,第106页)来获得指向ia中首元素和尾元素之后位置的指针,并传递给find。

  还可以在序列的子范围中查找,只需将指向子范围首元素和尾元素之后位置的迭代器(指针)传递给find。例如,下面的语句在ia[1]、ia[2]和ia[3]中查找给定元素:

//在从ia[1]开始,直至(但不包含)ia[4]的范围内查找元素 
auto result = find(ia + 1, ia + 4, val); 

1.1 算法如何工作

  为了弄清这些算法如何用于不同类型的容器,让我们更近地观察一下find。find的工作是在一个未排序的元素序列中查找一个特定元素。概念上,find应执行如下步骤:

1.访问序列中的首元素。

2.比较此元素与我们要查找的值。

3.如果此元素与我们要查找的值匹配,find返回标识此元素的值。

4.否则,find前进到下一个元素,重复执行步骤2和3。

5.如果到达序列尾,find应停止。

6.如果find到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。

这些步骤都不依赖于容器所保存的元素类型。因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。

1.2 迭代器令算法不依赖于容器, ……

  在上述find函数流程中,除了第2步外,其他步骤都可以用迭代器操作来实现:利用迭代器解引用运算符可以实现元素访问;如果发现匹配元素,find可以返回指向该元素的迭代器;用迭代器递增运算符可以移动到下一个元素;尾后迭代器可以用来判断find是否到达给定序列的末尾;find可以返回尾后迭代器(参见9.2.1节,第296页)来表示未找到给定元素。

1.3 ……, 但算法依赖于元素类型的操作

  虽然迭代器的使用令算法不依赖于容器类型,但大多数算法都使用了一个(或多个)元素类型上的操作。例如,在步骤2中,find用元素类型的=运算符完成=每个元素与给定值的比较。其他算法可能要求元素类型支持<运算符。不过,我们将会看到,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符。

关键概念:算法永远不会执行容器的操作
   泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。
   如我们将在10.4.1节(第358页)所看到的,标准库定义了一类特殊的迭代器,称为插入器(inserter)。与普通迭代器只能遍历所绑定的容器相比,插入器能做更多的事情。当给这类迭代器赋值时,它们会在底层的容器上执行插入操作。因此,当一个算法操作一个这样的迭代器时,迭代器可以完成向容器添加元素的效果,但算法自身永远不会做这样的操作。

2.初识泛型算法

  标准库提供了超过100个算法。幸运的是,与容器类似,这些算法有一致的结构。比起死记硬背全部100多个符法,理解此结构可以帮助我们更容易地学习和使用这些算法。

  除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。

  虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本的方法就足了解它们足否读取元素、改变元素或是重排元素顺序。

2.1 只读算法

  一些算法只会读取其输入范围内的元素,而从不改变元素。find就是这样一种算法,在10.1节练习(第337页)中使用的count函数也是如此。

  另一个只读算法是accumulate,它定义在头文件numeric中。accumulate函数接受三个参数,前两个指出了需求和的元素的范围,第三个参数是和的初值。假定vec是一个整数序列,则:

//对vec中的元素求和,和的初值是0
int sum = accumulate(vec.cbegin(), vec.cend(), 0); 

这条语句将sum设置为vec中元素的和,和的初值被设置为0。

accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。

2.1.1 算法和元素类型

  accumulate将第三个参数作为求和起点,这蕴含若一个编程假定:将元素类型加到和的类型上的操作必须是可行的。即,序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型。在上例中,vec中的元素可以是int,或者是double、long long或任何其他可以加到int上的类型。

  下面是另一个例子,由于string定义了+运算符,所以我们可以通过调用accumulate来将vector中所有string元素连接起来:

string sum = accumulate(v.cbegin(), v.cend(), string("")) ; 

此调用将v中每个元素连接到一个string上,该string初始时为空串。注意,我们通过第三个参数显式地创建了一个string。将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误。

//错误:const char*上没有定义+运算符
string sum = accumulate(v.cbegin (), v.cend (), ""); 

原因在于,如果我们传递了一个字符串字面值,用于保存和的对象的类型将是const char *。如前所述,此类型决定了使用哪个+运算符。由于const char*并没有+运算符,此调用将产生编译错误。

建议:对于只读取而不改变元素的算法,通常最好使用cbegin()和cend()(参见9.2.3节,第298页)。但是,如果你计划使用算法返回的迭代器未改变元素的值,就需要使用begin()和end()的结果作为参数。

2.1.2 操作两个序列的算法

  另一个只读算法是equal,用于确定两个序列是否保存相同的值。它将第一个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false。此算法接受三个迭代器:前两个(与以往一样)表示第一个序列中的元素范围,第三个表示第二个序列的首元素:

//roster2中的元素数目应该至少与roster1一样多
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

由于equal利用迭代器完成操作,因此我们可以通过调用equal来比较两个不同类型的容器中的元素。而且,元素类型也不必一样,只要我们能用=来比较两个元素类型即可。例如,在此例中,roster1可以是vector<string>, 而roster2是 list<const char*>。
  但是,equal基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。

警告:那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。

2.2 写容器元素的算法

  一些算法将新值赋予序列中的元素。当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。

  一些算法会自己向输入范围写入元素。这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。
  例如,算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素。

fill(vec.begin(), vec.end(), 0);//将每个元素重置为0//将容器的一个子序列设置为10
fill (vec.begin(), vec.begin() + vec.size()/2, 10); 

由于fill向给定输入序列中写入数据,因此,只要我们传递了一个有效的输入序列,写入操作就是安全的。

关键概念:迭代器参数
   一些算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器。例如,第一个序列可能保存于一个vector中,而第二个序列可能保存于一个list、deque、内置数组或其他容器中。而且,两个序列中元素的类型也不要求严格匹配。算法要求的只是能够比较两个序列中的元素。例如,对equal算法,元素类型不要求相同,但是我们必须能使用==来比较来自两个序列中的元素。
   操作两个序列的算法之间的区别在于我们如何传递第二个序列。一些算法,例如equal,接受三个迭代器:前两个表示第一个序列的范围,第三个表示第二个序列中的首元素。其他算法接受四个迭代器:前两个表示第一个序列的元素范围,后两个表示笫二个序列的范围。
   用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。例如,算法equal会将其第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误---equal会试图访问笫二个序列中末尾之后(不存在)的元素。

2.2.1 算法不检查写操作

  一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。例如,函数fill_n接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。我们可以用fill_n将一个新值赋予vector中的元素:

vector<int> vec;//空vector
//使用vec,赋子它不同值
fill_n(vec.begin(), vec.size(), O);//将所有元素重置为0 函数fill_n假定写入指定个元素是安全的。即,如下形式的调用
fill_n(dest, n, val) 

fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素。

  一个初学者非常容易犯的错误是在一个空容器上调用fill_n(或类似的写元素的算法):

vector<int> vec;//空向量
//灾难:修改vec中的10个(不存在)元素
fill_n(vec.begin(), 10, 0); 

这个调用是一场灾难。我们指定了要写入10个元素,但vec中并没有元素---它是空的。这条语句的结果是未定义的。

警告:向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。

2.2.2 介绍back_inserter

  一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器(insert iterator)。插入迭代器是一种向容器中添加元素的迭代器。通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。

  back_inserter,它是定义在头文件iterator中的一个函数。

  back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运绊符会调用push_back将一个具有给定值的元素添加到容器中:

vector<int> vec;//空向量
auto it = back_inserter(vec);//通过它赋值会将元素添加到vec中
*it = 42;//vec中现在有一个元素,值为42

我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:

vector<int> vec;//空向量
//正确:back_inserter创建一个插入迭代器,可用来向vec添加元素
fill_n(back_inserter(vec), 10, 0);//添加10个元素到vec

在每步迭代中,fill_n向给定序列的一个元素赋值。由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vec上调用push_back。最终,这条fill_n调用语句向vec的末尾添加了10个元素,每个元素的值都是0。

2.2.3 拷贝算法

  拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。

我们可以用copy实现内置数组的拷贝,如下面代码所示:

int a1[] = 0,1,2,3,4,5,6,7,8,9; 
int a2[sizeof(a1)/sizeof(*a1)]; //a2与a1大小一样
//ret指向拷贝到a2的尾元素之后的位置
auto ret = copy(begin(a1), end(a1), a2);//把a1的内容拷贝给a2

  此例中我们定义了一个名为a2的数组,并使用sizeof确保a2与数组a1包含同样多的元素(参见4.9节,第139页)。接下来我们调用copy完成从a1到a2的拷贝。在调用 copy后,两个数组中的元素具有相同的值。copy返回的是其目的位置迭代器(递增后)的值。即,ret恰好指向拷贝到a2的尾元素之后的位置。

  多个算法都提供所谓的”拷贝“版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。例如,replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:

//将所有值为0的元素改为42
replace(ilst.begin(), ilst.end(), 0, 42); 

此调用将序列中所有的0都替换为42。如果我们希望保留原序列不变,可以调用 replace_copy。此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:

//使用back_inserter按需要增长目标序列 
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42); 

此调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。

2.3 重排容器元素的算法

  某些算法会重排容器中元素的顺序,一个明显的例子是sort。调用sort会重排输入序列中的元素,使之有序,它是利用元素类型的<运算符来实现排序的。

  例如,假定我们想分析一系列儿童故事中所用的词汇。假定已有一个vector,保存了多个故事的文本。我们希望化简这个vector,使得每个单词只出现一次, 而不管单词在任意给定文档中到底出现了多少次。

  为了便于说明问题,我们将使用下面简单的故事作为输入:

the quick red fox jumps over the slow red turtle

turtle 给定此输入,我们的程序应该生成如下vector:

2.3.1 消除重复单词

  为了消除重复单词,首先将vector排序,使得重复的单词都相邻出现。一旦vector排序完毕,我们就可以使用另一个称为unique的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。由于算法不能执行容器的操作,我们将使用vector的erase成员来完成真正的删除操作:

void elimDups(vector<string> &words) 

    //按字典序排序words,以便查找重复单词
    sort(words.begin(), words.end()); 
    //unique重排输入范围,使得每个单词只出现一次
    //排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end unique = unique(words.begin(), words.end()); 
    //使用向量操作erase删除重复单词
    words.erase(end_unique, words.end()); 

sort算法接受两个迭代器,表示要排序的元素范围。在此例中,我们排序整个vector。完成sort后,words的顺序如下所示:

注意,单词 red和the各出现了两次。

2.3.2 使用unique

  words排序完毕后,我们希望将每个单词都只保存一次。unique算法重排输入序列,将相邻的重复项 “ 消除 “,并返回一个指向不重复值范围末尾的迭代器。调用unique后,vector将变为:

  words的大小并未改变,它仍有10个元素。但这些元素的顺序被改变了一相邻的重复元素被 “删除 ” 了。我们将删除打引号是因为unique并不真的删除任何元素,它只是狻盖相邻的重复元素,使得不重复元素出现在序列开始部分。unique返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在,但我们不知道它们的值是什么。

标准库算法对迭代器而不是容器进行操作。因此,算法不能(直接)添加或删除元素。

2.3.3 使用容器操作删除元素

  为了真正地删除无用元素,我们必须使用容器操作,本例中使用erase(参见9.3.3节,第311页)。我们删除从end_unique开始直至words末尾的范围内的所有元素。这个调用之后,words包含来自输入的8个不重复的单词。
  值得注意的是,即使words中没有重复单词,这样调用erase也是安全的。在此情况下,unique会返回words.end()。因此,传递给erase的两个参数具有相同的值:words.end()。迭代器相等意味着传递给erase的元素范围为空。删除一个空范围没有什么不良后果,因此程序即使在输入中无重复元素的情况下也是正确的。

3.定制操作

  很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或=运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。
  例如,sort算法默认使用元素类型的<运算符。但可能我们希望的排序顺序与<所定义的顺序不同,或是我们的序列可能保存的是未定义<运算符的元素类型(如Sales_data)。在这两种情况下,都需要重载sort的默认行为。

3.1 向算法传递函数

  作为个例子,假定希望在调用elimDups(参见10.2.3节,第343页)后打印vector的内容。此外还假定希望单词按其长度排序,大小相同的再按字典序排列。为了按长度重排vector,我们将使用sort的第二个版本,此版本是重载过的,它接受第三个参数,一此参数是个谓词(predicate)。

3.1.1谓词

  谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unary predicate,意味着它们只接受单一参数)和二元谓词(binary predicate,意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。

  接受一个二元谓词参数的sort版本用这个谓词代替<来比较元素。我们提供给sort的谓词必须满足将在11.2.2节(第378页)中所介绍的条件。当前,我们只需知道,此操作必须在输入序列中所有可能的元素值上定义一个致的序。我们在6.2.2节(第189页)中定义的isShorter就是一个满足这些要求的函数,因此可以将isShorter传递给sort。这样做会将元素按大小重新排序:

//比较函数,用来按长度排序单词
bool isShorter(const string &s1, const string &s2) 

    return si.size() < s2.size(); 

//按长度由短至长排序words
sort(words.begin(), words.end(), isShorter); 

如果words包含的数据与10.2.3节(第343页)中 一样,此调用会将words重排,使得所有长度为3的单词排在长度为4的单词之前,然后是长度为5的单词,依此类推。

3.1.2 排序算法

  在我们将words按大小重排的同时,还希望具有相同长度的元素按字典序排列。为了保持相同长度的单词按字典序排列,可以使用stable_sort算法。这种稳定排序算法维待相等元素的原有顺序。

  通常情况下,我们不关心有序序列中相等元素的相对顺序,它们毕竟是相等的。但是,在本例中,我们定义的 ” 相等 “ 关系表示 “ 具有相同长度 ”。而具有相同长度的元素,如果看其内容,其实还是各不相同的。通过调用stable_sort,可以保持等长元素间的字典序:

elimDups(words);//将words按宇典序重排, 并消除重复单词
//按长度重新排序,长度相同的单词维持宇典序
stable_sort(words.begin(), words.end(), isShorter); 
for(const auto &s : words)//无须拷贝字符串
    cout << s << " ";//打印每个元素, 以空格分隔
cout << endl; 

假定在此调用前words是按字典序排列的,则调用之后,words会按元素大小排序,而长度相同的单词会保持字典序。如果我们对原来的vector内容运行这段代码,输出为:

fox red the over slow jumps quick turtle

3.2 lambda表达式

  根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两 个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。

  一个相关的例子是,我们将修改10.3.1节(第345页)中的程序,求大于等于一个给定长度的单词有多少。我们还会修改输出,使程序只打印大于等于给定长度的单词。

我们将此函数命名为biggies,其框架如下所示:

void biggies(vector<string> &words, vector<string>::size_type sz) 

    elimDups(words);//将words按字典序排序,删除重复单词//按长度排序,长度相同的单词维持字典序
    stable_sort(words.begin(), words.end(), isShorter);//获取一个迭代器,指向第一个满足size() >= sz的元素
    //计算满足 size >= sz的元素的数目
    //打印长度大于等于给定值的单词每个单词后面接一个空格

  我们的新问题是在vector中寻找第一个大于等于给定长度的元素。一旦找到了这个元素,根据其位置,就可以计算出有多少元素的长度大于等于给定值。

  我们可以使用标准库find_if算法来查找第一个具有特定大小的元素。类似find(参见10.1节,第336页),find_if算法接受一对迭代器,表示一个范围。但与find不同的是,find_if的第三个参数是一个谓词。find_if算法对输入序列中的每个元素调用给定的这个谓词。它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。

  编写一个函数,令其接受一个string 和一个长度,并返回一个bool值表示该string的长度是否大于给定长度,是一件很容易的事情。但是,find_if接受一元谓词的一个元素调用它。没有任何办法能传递给它第二个参数来表示长度。为了解决此问题,需要使用另外一些语言特性。

3.2.1 介绍lambda

  我们可以向一个算法传递任何类别的可调用对象(callable object)。 对于一个对象或一个表达式,如果可以对其使用调用运算符(参见1.5.2节,第21页),则称它为可调用的。即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。

  到目前为止,我们使用过的仅有的两种可调用对象是函数和函数指针(参见6.7节,第221页)。还有其他两种可调用对象:重载了函数调用运算符的类,我们将在14.8节(第506页)介绍,以及lambda表达式(lambda expression)。

  一个 lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式

[capture list](parameter list) -> return type  function body

其中,capture list(捕获列表)是一个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。

3.2.2向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(),并根据两者的相对大小返回一个布尔值。

如下所示,可以使用此lambda来调用stable_sort:

//按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), [] (const string&a, const string &b) return a.size() < b.size(); );  

当stable_sort需要比较两个元素时,它就会调用给定的这个lambda表达式。

3.2.3 使用捕获列表

  我们现在已经准备好解决原来的问题了一编写一个可以传递给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提供一个空捕获列表,则代码会编译错误:

//错误:sz未捕获
[] (const string &a) 
注意:一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

3.2.4 调用find_if

  使用此lambda,我们就可以查找第一个长度大于等于sz的元素:

//获取一个迭代器,指向第一个满足 size()>= sz的元素
auto wc = find_if(words.begin(), words.end(), [sz] (canst string &a)  return a.size() >= sz; ); 

  这里对find_it的调用返回一个迭代器,指向第一个长度不小于给定参数sz的元素。如果这样的元素不存在,则返回words.end()的一个拷贝。
  我们可以使用find_if返回的迭代器来计算从它开始到words的末尾一共有多少个元素(参见3.4.2节,第99页):

//计算满足 size >= sz 的元素的数目
auto count = words. end() - we; 
cout << count << " " << make_plural(count, "word", "s") << " of length "<< sz << " or longer" << endl; 

我们的输出语句调用make_plural(参见6.3.2节,第201页)来输出“ word”或“ words",具体输出哪个取决于大小是否等于1。

3.2.5 for_each算法

  问题的最后一部分是打印words中长度大于等于sz的元素。为了达到这一目的,我们可以使用for_each算法。此算法接受一个可调用对象,并对输入序列中每个元素调用此对象

//打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each(wc, words.end(), [](const string &s)cout << s << " ";); 
cout << endl; 

  此lambda中的捕获列表为空,但其函数体中还是使用了两个名字:s和cout,前者是它自己的参数。
  捕获列表为空,是因为我们只对lambda所在函数中定义的(非static)变量使用捕获列表。一个lambda可以直接使用定义在当前函数之外的名字。在本例中,cout不是定义在biggies中的局部名字,而是定义在头文件iostream中。因此,只要在biggies出现的作用域中包含了头文件iostream,我们 的lambda就可以使用cout。

注意:捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。

3.2.6 完整的biggies

到目前为止, 我们已经解决了程序的所有细节, 下面就是完整的程序:

void biggies(vector<string> &words, vector<string>::size_type 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 &a)  return a.size() >= sz; );
    //计算满足 size >= sz 的元素的数目
    auto count = words.end() - wc; 
    cout << count << " " << make_plural(count, "word", "s") << " of length "<< sz << " or longer" << endl; 
    //打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(wc, words.end(), [](const string &s) cout << s << " ";); 
    cout << endl; 

3.3 lambda捕获和返回

  当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。我们将在14.8.1节(第507页)介绍这种类是如何生成的。目前,可以这样理解,当向一个函数传递一个lambda时,同时定义了一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。

  默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。

3.3.1 值捕获

  类似参数传递,变量的捕获方式也可以是值或引用。表10.1(第352页)列出了几种不同的构造捕获列表的方式。到目前为止,我们的lambda采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:

void fcnl()

    size_t v1 = 42;//局部变量
    //将v1拷贝到名为f的可调用对象
    auto f = [v1]  return v1; ; 
    v1 = 0; 
    auto j = f();//j为42;f保存了我们创建它时v1的拷贝

由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。

3.3.2 引用捕获

我们定义lambda时可以采用引用方式捕获变量。例如:

void fcn2() 

    size_t v1 = 42;//局部变量
    //对象f2包含vl的引用
    auto f2 = [&v1]  return v1;; 
    v1 = 0; 
    auto j = f2();//为0;f2保存v1的引用,而非拷贝

  v1之前的&指出v1应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当lambda返回v1时,它返回的是v1指向的对象的值。
  引用捕获与返回引用(参见6.3.2节,第201页)有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。lambda捕获的都是局部变量,这些变蜇在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

  引用捕获有时是必要的。例如,我们可能希望biggies函数接受引用,用来输出数据,并接受一个个ostream的字符作为分隔符:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c =\' \') 

    //与之前例子一样的重排words的代码
    //打印count的语句改为打印到OS
    for_each(words.begin(), words.end(), [&os, c] (const string&s)  os << s << c; ); 

我们不能拷贝ostream对象(参见8.1.1节,第279页),因此捕获OS的唯一方法就是捕获其引用(或指向OS的指针)。
  当我们向一个函数传递一个lambda时,就像本例中调用for_each那样,lambda会立即执行。在此情况下,以引用方式捕获OS没有问题,因为当for_each执行时,biggies中的变量是存在的。
  我们也可以从一个函数返回lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。

警告:当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。
建议:尽量保持lambda的变亮捕获简单化
   一个lambda捕获从lambda被创建(即,定义lambda的代码执行时)到lambda自身执行(可能有多次执行)这段时间内保存的相关信息。确保lambda每次执行的时候这些信息都有预期的意义,是程序员的责任。
   捕获一个普通变量,如int、string或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。
   如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值。在lambda从创建到它执行的这段时间内,可能有代码改变绑定的对象的值。也就是说,在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在lambda执行时,该对象的值可能已经完全不同了。
   一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且、 如果可能的话,应该避免捕获指针或引用。

3.3.3 隐式捕获

  除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中一写个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。例如,我们可以重写传递给find_if的lambda:

//sz为隐式捕获,值捕获方式
wc = find_if(words.begin(), words.end(), [=](const string &s) returns.size() >= sz; );  

  如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, 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; ); 

当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值。
  当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。

表10.1,lambda捕获列表
[] 空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量后才能使用它们
[names] names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了&,则采用引用捕获方式
[&] 隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用
[=] 隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值
[&, identifer_list] identifer_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获identifer_list中的名字前面不能使用&
[=, identifer_list] identifer_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifer_list中的名字不能包括this,且这些名字 之前必须使用&

3.3.4 可变lambda

  默认情况下, 对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变 一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:

void fcn3()

    size_t v1 = 42;//局部变量
    //f可以改变它所捕获的变量的值
    auto f = [v1]()mutable  return ++v1;) ;
    v1 = 0; 
    auto j = f();//j为43

一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个const类型还是一个非const类型:

void fcn4() 

    size_t v1 = 42;//局部变量
    //v1是一个非const变量的引用
    //可以通过f2中的引用来改变它
    auto f2 = [&v1]return ++v1;;
    v1 = 0; 
    auto j = f2();//j为1

3.3.5 指定lambda返回类型

  到目前为止,我们所编写的lambda都只包含单一的return语句。因此,我们还未遇到必须指定返回类型的情况。默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其他返回void的函数类似,被推断返回void的lambda不能返回值。

  下面给出了一个简单的例子,我们可以使用标准库transform算法和一个lambda来将一个序列中的每个负数替换为其绝对值:

transform(vi.begin(), vi.end(), vi.begin(), [](int i)  return i < 0 ? -i : i; ); 

函数transforrn接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform将输入序列中每个元素替换为可调用对象操作该元素得到的结果。

  在本例中,我们传递给transform一个lambda,它返回其参数的绝对值。lambda体是单一的return语句,返回一个条件表达式的结果。我们无须指定返回类型,因为可以根据条件运算符的类型推断出来。

  但是,如果我们将程序改写为看起来是等价的if语句,就会产生编译错误:

//错误:不能推断lambda的返回类型
transform(vi.begin(), vi.end(), vi.begin(), [](int i) if (i < 0) return -i; else return i; ); 

编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。

  当我们需要为一个lambda定义返回类型时, 必须使用尾置返回类型(参见6.3.3节,第206页):

transform(vi.begin(), vi.end(), vi.begin(), [] (int i) -> int if (i < 0) return -i; else return i;); 

  在此例中,传递给transform的第四个参数是一个lambda,它的捕获列表是空的,接受单一int参数,返回一个int值。它的函数体是一个返回其参数的绝对值的if语句。

3.4 参数绑定

  对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。

  如果lambda的捕获列表为空,通常可以用函数来代替它。如前面章节所示,既可以用一个lambda,也可以用函数isShorter来实现将vector中的单词按长度排序。类似的,对于打印vector内容的lambda,编写一个函数来替换它也是很容易的事情,这个函数只需接受一个string并在标准输出上打印它即可。

  但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如,我们用在find_if调用中的lambda比较一个string和一个给定大小。我们可以很容易地编写一个完成同样工作的函数:

bool check_size(const string &s, string::size—type sz)

    return s.size() >= sz; 

但是,我们不能用这个函数作为find_it的一个参数。如前文所示,find_if接受一个一元谓词,因此传递给find_it的可调用对象必须接受单一参数。biggies传递给find_it的lambda使用捕获列表来保存sz。为了用check_size来代替此lambda,必 须解决如何向sz形参传递一个参数的问题。

3.4.1 标准库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中的参数。

  arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,依此类推。

3.4.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&。因此,调用check6必须传递给它一个string类型的参数,check6会将此参数传递给check_size。一个

strings= "hello"; 
bool b1= check6(s);//check6(s)会调用check_size(s, 6) 

使用bind,我们可以将原来基于lambda的find_if调用:

auto wc = find_if(words.begin(), words.end(), [sz](const string &a)

替换为如下使用check_size的版本:

auto we= find_if(words.begin(), words.end(), bind(check_size, _1, sz)); 

此bind调用生成一个可调用对象,将check_size的第二个参数绑定到sz的值。当find_if对words中的string调用这个对象时,这些对象会调用check_size,将给定的string和sz传递给它。因此,find_if可以有效地对输入序列中每个string调用check_size,实现string的大小与sz的比较。

3.4.3 使用placeholders名字

  名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间(参见3.1节,第74页)中。为了使用这些名字,两个命名空间都要写上。与我们的其他例子类似,对bind的调用代码假定之前已经恰当地使用了using声明。例如,_1对应的using声明为:

using std::placeholders:: _1; 

此声明说明我们要使用的名字_1定义在命名空间placeholders中,而此命名空间又定义在命名空间std中。
  对每个占位符名字,我们都必须提供一个单独的using声明。编写这样的声明很烦人,也很容易出错。可以使用另外一种不同形式的using语句(详细内容将在1.82.2节(第702页)中介绍),而不是分别声明每个占位符,如下所示:

using names pace namespace_name; 

这种形式说明希望所有来自namespace_name的名字都可以在我们的程序中直接使用。例如:

using namespace std::placeholders; 

使得由placeholders定义的所有名字都可用。 与find函数一样,placeholders命名空间也定义在functional头文件中。

3.4.4 bind的参数

如前文所述,我们可以用bind修正参数的值。更一般的,可以用bind绑定给定可调用对象中的参数或重新安排其顺序。例如,假定f是一个可调用对象,它有5个参数,则下面对find的调用:

//g是一个有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, 1); 

  生成一个新的可调用对象,它有两个参数,分别用占位符_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)

3.4.5 用 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)一样。

3.4.6 绑定引用参数

  默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定参数的类型无法拷贝。

例如,为了替换一个引用方式捕获ostream的lambda:

//os是一个局部变量,引用一个输出流
//c是一个个局部变量,类型为char
for_each(words.begin(), words.end(), [&os, c] (const string &s)  os << s << c; ); 

可以很容易地编写一个函数,完成相同的工作:

ostream &print(ostream &os, const string &s, char c)

    return os << s << c; 

但是,不能直接用bind来代替对os的捕获:

//错误:不能拷贝os
for_each(words.begin(), words.end(), bind(print, os, _1, \' \')); 

原因在于bind拷贝其参数,而我们不能拷贝一个ostream。如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数:

for_each(words.begin(), words.end(), bind(print, ref(os), _1, \' \' )); 

函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。与bind一样,函数ref和cref也定义在头文件functional中。

向后兼容:参数绑定
----------------------------------------------------------------------
旧版本C++提供的绑定函数参数的语言特性限制更多,也更复杂。标准库定义了两个分别名为bind1st和bind2nd的函数。类似bind,这两个函数接受一个函数作为参数,生成一个新的可调用对象,该对象调用给定函数,并将绑定的参数传递给它。但是,这些函数分别只能绑定第一个或笫二个参数。由于这些函数局限太强,在新标准中已被弃用(deprecated)。所谓被弃用的特性就是在新版本中不再支持的特性。新的C++程序应该使用bind。

4.再探迭代器

  除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。这些迭代器包括以下几种。

●插入迭代器(insert iterator):这些迭代器被绑定到一个容器上,可用来向容器插入元素。

●流迭代器(stream iterator): 这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。

●反向迭代器 (reverse iterator): 这些迭代器向后而不是向前移动。除了 forward_list之外的标准库容器都有反向迭代器。

●移动迭代器(move iterator): 这些专用的迭代器不是拷贝其中的元素,而是移动它 们。

4.1 插入迭代器

  插入器是一种迭代器适配器(参见9.6节,第329页),它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。表10.2列出了这种迭代器支持的操作。

表10.2: 插入迭代器操作
it = t 在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t)或c.insert(t,p),其中p为传递给inserter的迭代器位置
*it,++it,it++ 这些操作虽然存在,但不会对it做任何事情。每个操作都返回it

插入器有三种类型,差异在于元素插入的位置:

●back_inserter(参见10.2.2节,第341页)创建一个使用push_back的迭代 器。
●front_inserter创建一个使用push_front的迭代器。
●inserter创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

注意:只有在容器支持push_front的情况下,我们才可以使用front_inserter。类似的,只有在容器支待push_back的情况下,我们才能使用back_inserter。

  理解插入器的工作过程是很重要的:当调用inserter(c,iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指向的元素之前的位置。即,如果it是由inserter生成的迭代器,则下面这样的赋值语句

*it = val;

其效果与下面代码一样

it = c.insert(it, val);//it指向新加入的元素 
++it;//递增止使它指向原来的元素

  front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样,我们使用front_inserter时,元素总是插入到容器第一个元素之前。即使我们传递给 inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:

list<int> 1st = 1,2,3,4;
list<int> lst2, lst3;//空list
//拷贝完成之后, ls七2 包含 4 3 2 1 
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
//拷贝完成之后, lst3 包含 1 2 3 4 
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin())); 

当调用front_inserter(c)时,我们得到一个插入迭代器,接下来会调用push_front。当每个元素被插入到容器c中时,它变为c 的新的首元素。因此,front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserter和back_inserter则不会。

4.2 iostream 迭代器

  虽然iostream 类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器(参见8.1节, 第 278页)。istream_iterator (参见表10.3) 读取输入流,otream_iterator(参见表10.4节,第361页)向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

4.2.1 istream_iterator操作

  当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符,当创建一个流。当然,我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。

istream_iterator<int> int_it(cin);//从cin读取int 
istream_iterator<int> int_eof;//尾后迭代器 
ifstream in("afile"); 
istream_iterator<st

c++泛型算法

顺序容器只定义了很少的操作,为了能做其他更多有用的操作:查找特定元素,替换或删除某一特定值,重排元素顺序等。泛型算法是一些经典算法的公共接口

1.概述

大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。

泛型算法不会执行容器的操作,只会运行于迭代器之上,执行迭代器的操作 。这样就导致算法不能改变容器的大小,也就不能直接添加或删除元素了。因此标准库定义了一种叫插入器的特殊迭代器来完成向容器添加元素的效果。

2.初识泛型算法

标准库提供了超过100个算法。

a.只读算法

只读取,如find,count,accumulate,equal。equal算法假定第二个序列至少和第一个序列一样长,只接受三个迭代器,前两个表示第一个序列的范围,第三个是第二个序列的首元素。

b.写容器元素的算法

使用这类算法是,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。算法fill接受一对迭代器表示一个范围,接受一个值作为第三个参数。

函数fill_n接受一个单迭代器、一个计数值和一个值。但是不能在空容器上调用fill_n,因为向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。

一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。使用一般的迭代器想容器元素赋值时,容器内元素的值呗改变。但是使用插入迭代器赋值时,一个与该值相等的元素被添加到容器中。

插入迭代器如back_inserter,此函数返回与容器绑定的插入迭代器,当向插入迭代器赋值是,会调用push_back将一个具有给定值的元素添加到容器中,如以下代码:

vector<int>  vec;//空容器

auto it=back_inserter(vec);

*it = 42;//vec中有个元素,值42

拷贝算法:接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。copy算法,该算法返回的是目的位置迭代器的值。replace算法接受四个参数,前两个是迭代器,第三个是要搜索的值,第四个是新值。replace_copy算法保持原序列不变,只是拷贝一份原序列并替代。

c.重排容器元素的算法

sort会重排元素,使之有序。unique会重排输入序列,将相邻的重复项移到最后面,并返回一个指向不重复值范围末尾的迭代器。erase用于删除元素。

3.定制操作

a.比如,sort默认从小到大排序,但是我们的要求可能不同,因此需要重载sort的默认行为。为了按长度重排vector,要使用sort的第二个版本,它接受第三个参数,此参数是一个谓词。

谓词:谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。根据参数个数分为一元谓词和二元谓词

lambda:我们可以向一个算法传递任何类别的可调用对象。到目前为止,使用过的两种可调用对象是函数和函数指针。还有其他两种可调用对象:重载了函数调用运算符的类以及lambda表达式。

一个lambda表达式表示一个可调用的代码单元。可将其理解为一个未命名的内联函数。表达形式如下:

[capture list] (parameter list)  -> return type {function body}//  capture list(捕获列表)是定义的局部变量的列表,lambda必须使用尾置返回指定返回类型。必须包括捕获列表和函数体。

lambda注:a.不能有默认参数。b.捕获列表捕获它所在函数的局部变量。程序如下:

程序功能:查找第一个长度大于等于sz的元素

auto wc = find_if(words.begin(),words.end(),[sz](const string &a){return a.size()>=sz;})

[=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字,例如cout。

for_each算法接受一个可调用对象,并对输入序列中每个元素调用此对象。

b.lambda捕获和返回

值捕获,由于变量被捕获时是在lambda创建时被拷贝,随后对其修改不会影响到lambda内对应的值。只能用拷贝的值,不能改变。若要改变,用mutable。

引用捕获,使用此方法时对变量的修改会影响到lambda内对应的值。

隐式捕获,[=]采用值捕获,[&]采用引用捕获。

可变lambda:默认情况下,拷贝的变量值,lambda不会改变其值。如果希望改变,就必须在参数列表后加上关键字mutable。

指定lambda返回类型:如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。为了使lambda返回特定类型,就需要使用尾置返回类型->。

c.参数绑定

对于只在一两个地方使用的简单操作,lambda表达式是最有用的。如果要在很多地方使用相同的操作,通常应该定义一个函数。

但是函数一般接受的参数较多,但是一般的算法只接受一元谓词(一个参数的函数),为了解决这个矛盾,我们使用bind的函数。

auto newC=bind(c,list);//将多个参数转化成一个参数。

auto check6 = bind(check_size,_1,6);//向函数check_size绑定两个参数,生成一个可调用对象。为了使用_1,需要声明:using std::placeholders::_1;

bind参数:auto g=bind(f,a,b,_2,c,_1);    这样的话使用g(_1,_2);将会映射为f(a,b,_2,c,_1);

绑定引用参数使用ref函数。

4.再探迭代器

迭代器类型:a.插入迭代器b.流迭代器c.反向迭代器d.移动迭代器。

a.插入迭代器

三种类型:back_inserter//创建一个使用push_back的迭代器   front_inserter//创建一个使用push_front的迭代器    inserter//创建一个使用insert的迭代器

例如:it=inserter(c,iter);//得到插入迭代器it。*it=val;//在it指向的元素前加入val

b.iostream迭代器

两种类型:istream_iterator读取输入流,ostream_iterator向一个输入流写数据。示例程序:

istream_iterator<int> int_iter(cin),eof;//从cin读取int   vector<int> vec(in_iter,eof);//构造vec

c.反向迭代器

crbegin()表示容器的尾部。若r为反向迭代器,为得到正常的迭代器,使用r.base()就能得到普通的迭代器。

5.泛型算法结构

(1)迭代器类别:输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器。

a.输入迭代器

find和accumuluate要求输入迭代器,而istream_iterator是一种输入迭代器。

b.输出迭代器

copy的第三个参数是输出迭代器,ostream_iterator是一种输出迭代器。

c.前向迭代器

replace要求前向迭代器,forward_list上的迭代器是前向迭代器。

d.双向迭代器(reverse)

e.随机访问迭代器(sort,  array、deque、string、vector的迭代器)

 (2)算法形参模式

alg(beg,end,other args);

alg(beg,end,dest,other args);//dest也是迭代器

alg(beg,end,beg2,other args);

alg(beg,end,beg2,end2,other args);

6.特定容器算法

list和forward_list定义了几个成员函数的算法,他们定义了独有的sort,merge,remove,reserse和unique。

链表类型还定义了splice算法。链表的特有版本的算法会改变底层容器,这是与通用版算法的区别。

以上是关于97.泛型算法的主要内容,如果未能解决你的问题,请参考以下文章

c++泛型算法

算法刷题AcWing 97. 约数之和——递推

接口中的C#泛型,编写泛型算法

《算法零基础100例》(第97例) 动态规划 - 0/1背包

泛型算法C++

泛型