C++ Primer 10 泛型算法

泛型算法

1 概述

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

一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。

int val = 42;  // 将查找的值
// 如果在 vec 中找到想要的元素,则返回结果指向它,否则返回结果为 vec.cend()
auto result = find(vec.cbegin(), vec.cend(), val);
算法如何工作

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

  1. 访问序列中的首元素。
  2. 比较此元素与我们要查找的值。
  3. 如果此元素与我们要查找的值匹配,find 返回标识此元素的值。
  4. 否则,find 前进到下一个元素,重复执行步骤2和3。
  5. 如果到达序列尾,find 应停止。
  6. 如果 find 到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。

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

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

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


2 初识泛型算法

2.1 制度算法

一些算法只会读取其输入范围内的元素,而从不改变元素。find 就是这样一种算法,count 函数也是如此。另一个只读算法是 accumulate,它定义在头文件 numeric 中。accumulate 函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。

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

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

accumulate 算法

accumulate 将第三个参数作为求和起点,这蕴含着一个编程假定:将元素类型加到和的类型上的操作必须是可行的。即,序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型。

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

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

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

// roster2 中的元素数目应该至少与 rosterl 一样多
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

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

fill_n

函数 fill_n 接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。

我们可以用 fill_n 将一个新值赋予 vector 中的元素:

vector<int> vec;  // 空 vector
// 使用vec,赋予它不同值
fill_n(vec.begin(), vec.size() , 0) ;  // 将所有元素重置为 0

函数 fill_n 假定写入指定个元素是安全的。即,如下形式的调用

fill_n(dest, n, val)

fill_n 假定 dest 指向一个元素,而从 dest 开始的序列至少包含 n 个元素。注意,不要再一个空容器上调用 fill_n (或类似的写元素的算法)。

back_inserter

插入迭代器是一种向容器中添加元素的迭代器。通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。

back_inserter 定义在头文件 iterator 中,接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用 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

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

copy 拷贝算法

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

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

copy 返回的是其目的位置迭代器(递增后)的值。即,ret 恰好指向拷贝到 a2 的尾元素之后的位置。

replace 和 replace_copy

replace 算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:

// 将所有值为0的元素改为42
replace (i1st.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 重排容器,unique 消除重复元素

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

void elimeDups(vector<stirng> &word) {
	// 按字典顺序排序 words
	sort(word.begin(), word.end());
	// unique 重排输入范围,使得每个单词只出现一次
	// 排列在范围的前部,返回值指向不重复区域之后一个位置的迭代器
	auto end_unique = unique(word.begin(), word.end());
	// 使用向量操作 erase 删除重复单词
	words.erase(end_unique, words.end());
}

sort 算法接受两个迭代器,表示要排序的元素范围。调用 sort 会重排输入序列中的元素,使之有序,它是利用元素类型的 < 运算符 来实现排序的。

unique 算法重拍输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器,调用 unique 后,words 的大小并未改变,它仍有 10 个元素,但这些元素的顺序被改变了,相邻重复元素被“删除”了。这个删除并不是因为 unique 真的删除了元素,他只是覆盖相邻的重复元素,使得不重复元素是出现在序列开始部分。unique 返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在,但我们不知道它们的值是什么。

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

erase 容器操作,删除元素

为了真正地删除无用元素,我们必须使用容器操作,erase。即使传递给 erase 的两个参数具有同样的值,也是安全的。迭代器相等一位置传递给 erase 的元素范围为空,删除一个空范围没有什么不良后果。


3 定制操作

很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的 < 或 == 运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。

例如,sort 算法默认使用元素类型的 < 运算符。但可能我们希望的排序顺序与 < 所定义的顺序不同,或是我们的序列可能保存的是未定义 < 运算符的元素类型(如Sales_data)在这两种情况下,都需要重载 sort 的默认行为。

3.1 向算法传递函数

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

sort(words.begin(), words.end(), isShorter);
谓词

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

bool isShorter(const string &s1, const string &s2) {
	return s1.size() < s2.size();
}
排序算法

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

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

elimDups(words);  // 将 words 按字典序重排,并消除重复单词
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);

stable_sort() 函数完全可以看作是 sort() 函数在功能方面的升级版。换句话说,stable_sort() 和 sort() 具有相同的使用场景,就连语法格式也是相同的,只不过前者在功能上除了可以实现排序,还可以保证不改变相等元素的相对位置。

3.2 lambda 表达式

介绍 lambda

我们可以向一个算法传递任何类别的 可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用。
注: 调用运算符是一对圆括号,里面放置实参列表(可能为空)。

到目前为止,我们使用过的仅有的两种可调用对象是 函数函数指针。还有其他两种可调用对象:重载了函数调用运算符的类,以及 lambda 表达式

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

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

capture list(捕获列表)是一个 lambda 所在函数中定义的局部变量的列表(通常为空);return type、parameter list 和 function body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda 必须使用尾置返回来指定返回类型。

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体

auto f = [] { return 42; }

lambda 的调用方式与普通函数的调用方式相同,都是使用调用运算符

cout << f() << endl;  // 打印 42

在 lambda 中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用 f 时,参数列表是空的。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。 如果函数体只是一个 return 语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为 void。
注: 如果 lambda 的函数体包含任何单一 return 语句之外的内容,且未指定返回 类型,则返回 void。

向 lambda 传递参数

与一个普通函数调用类似,调用一个 lambda 时给定的实参被用来初始化 lambda 的形参。通常,实参和形参的类型必须匹配。但与普通函数不同, lambda 不能有默认参数。因此,一个 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(word.begin(), word.end(),
	[] (const string &a, const string &b) { 
		return a.size()  <  b.size(); 
	}
);

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

使用捕获列表

虽然一个 lambda 可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个 lambda 通过将局部变量包含在其捕获列表中来指出将会使用这些变量。 捕获列表指引 lambda 在其内部包含访问局部变量所需的信息。

在本例中,我们的 lambda 会捕获 sz , 并只有单一的 string 参数。其函数体会将 string 的大小与捕获的sz的值进行比较:

[sz](const string &a) { 
	return a.size() >= sz;
};

注: 一个 lambda 只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

find_if 算法

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

for_each 算法

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

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

在本例中,cout 不是定义在 biggies 中的局部名字,而是定义在头文件 iostream 中。因此,只要在 biggies 出现的作用域中包含了头文件 iostream,我们的 lambda 就可以使用 cout。
注: 捕获列表只用于局部非 static 变量,lambda 可以直接使用局部 static 变量和它所在函数之外声明的名字。

完整的 biggies
void biggies(std::vector<std::string>&words,
         std::vector<std::string>::size_type sz) {
         
    // 将 words 按照字典排序并且删除重复的单词
    elimDups(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;
}

3.3 lambda 捕捉和返回

当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用 auto 定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。

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

值捕获

类似参数传递,变量的捕获方式也可以是值或引用。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝:

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

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

引用捕获

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

void fcn2() {
	size_t v1 = 42;  // 局部变量
	// 将 f 包含 v1 的引用
	auto f = [&v1] { return v1; }
	v1 = 0;
	auto j = f(); // j 为 0; f2 保存 v1 的引用,而非拷贝
}

v1 之前的 & 指出 v1 应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在 lambda 函数体内使用此变量时,实际上使用的是引用所绑定的对象。

引用捕获与返回引用,有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 lambda 执行的时候是存在的。 lambda 捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

我们也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一 个类对象,该类含有可调用对象的数据成员。如果函数返回一个 lambda,则与函数不能返回一个局部变量的引用类似,此 lambda 也不能包含引用捕获。

建议: 尽量保持 lambda 的变量捕获简单化
一个 lambda 捕获从 lambda 被创建(即,定义 lambda 的代码执行时) 到 lambda 自身执行(可能有多次执行)这段时间内保存的相关信息。确保 lambda 每次执行的时候 这些信息都有预期的意义,是程序员的责任。一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。

隐式捕获

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

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

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

// os 隐式捕获,引用捕获方式;c 显式捕获,值捕获方式
for_each(word.begin(), word.end(),
	[&, c](const string &s) {
		os << s << c;
	}
);

// os 显式捕获,引用捕获方式;c 隐式捕获,值捕获方式
for_each(word.begin(), word.end(),
	[=, &os](const string &s) {
		os << s << c;
	}
);

当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 & 或 =。此符号指定了默认捕获方式为引用或值。

当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用 &。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。

可变 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 = f(); // j 为 1
}
指定 lambda 返回类型

默认情况下,如果一个 lambda 体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void。与其他返回 void 的函数类似,被推断返回 void 的 lambda 不能返回值。

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

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

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

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

但是,如果我们将程序改写为看起来是等价的 if 语句,就会产生编译错误。编译器推断这个版本的 lambda 返回类型为 void,但它返回了一个 int。:

// 错误,返回的类型和编译推导出来的类型不一致。
transform(vi.begin(), vi.end(), vi.begin(),
	[](int i) {
		if (i < 0) return -i;
		else return i;
	}
);

当我们需要为一个lambda 定义返回类型时,必须使用尾置返回类型:

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

3.4 参数绑定

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

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

bool check_size(const string &s, string::size_type sz) {
	return s.size() >= sz;
}

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

标准库 bind 函数

我们可以解决向 check_size 传递一个长度参数的问题,方法是使用一个名为 bind 的标准库函数,它定义在头文件 functional 中。可以将 bind 函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来"适应”原对象的参数列表。

调用 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 为第二个参数,依此类推。

绑定 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。

string s = "hello"; 
bool bl = 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 wc = find_if(words.begin(), words.end(),
	 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 命名空间。为了使用这些名字,两个命名空间都要写上:

std::placeholders::_1;

此声明说明我们要使用的名字 _1 定义在命名空间 placeholders 中,而此命名空间又定义在命名空间 std 中。

bind 的参数

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

// 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(_1, _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:

// 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 引用的类。函数 ref 和 cref 也定义在头文件 functional 中。


4 再探迭代器

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

  • 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。
  • 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的 IO 流。
  • 反向迭代器: 这些迭代器向后而不是向前移动 。除了 forward_list 之外的标准库容器都有反向迭代器。
  • 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。

4.1 插入迭代器

插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素

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

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

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

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

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

4.2 iostream 迭代器

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

istream_iterator 操作

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

istream_iterator<int> in_iter(cin);  // 从 cin 读取 int
istream_iterator<int> eof;  // istream 尾后迭代器
while(in_iter != eof)
	// 后置递增运算读取流,返回迭代器的旧值
	// 解引用迭代器,获取从流读取的前一个值
	vec.push_back(*in_iter++);

后置递增运算回从流中读取下一个值,向前推进,但返回的时迭代器的旧值。迭代器的旧值包含了从流中读取的前一个值,对迭代器进行解引用就能获得此值。

我们可以将程序重写为如下形式,

istream_iterator<int> in_iter(cin);  // 从 cin 读取 int
istream_iterator<int> eof;  // istream 尾后迭代器
vector<int> vec(in_iter, eof);  // 从迭代器范围构造 vec

本例中我们用一对表示元素范围的迭代器来构造vec。这两个迭代器是 istream_iterator,这意味着元素范围是通过从关联的流中读取数据获得的。这个构造函数从 cin 中读取数据,知道遇到文件尾或者遇到一个不是 int 的数据为止。从流中读取的数据被用来构造 vec。

使用算法操作流迭代器

下面是一个例子,我们可以用一对 istream_iterator 来调用 accumulate

istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
istream_iterator 允许使用懒惰求值

当我们将一个 istream_iterator 绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了

对于大多数程序来说,立即读取还是延迟读取没什么差别。但是,如果我们创建了一个 istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。

ostream_iterator 操作

我们可以对任何具有输出运算符的类型定义 ostream_iterator。当创建一个 ostream_iterator 时,我们可以提供(可选的)第二个参数,它是一个字符串,在输出每个元素后都会答应此字符串。此字符串必须是一个 C 风格字符串(一个字符串字面常量或一个指向以空字符结尾的字符数组的指针)。必须将 ostream_iterator 绑定到一个指定的流,不允许空的或表示尾后位置的 ostream_iterator。

ostream_iterator<int> out_iter(cout, " ");
for(auto e: vec){
    *out_iter++ = e;  // 赋值语句实际上将元素写到 cout
}

此程序将 vec 中的每个元素写到 cout,每个元素后加一个空格。每次向 out_iter 赋值时,写操作就会被提交,值得注意的是,当我们向 out_iter 赋值时,可以忽略解引用和递增运算,故,循环可以重写成:

for(auto e: vec){
    out_iter = e;  // 赋值语句实际上将元素写到 cout
}

注: 解引用和递增实际上对 ostream_iterator 对象不做任何事情,因此忽略它们对我们的程序没有任何影响,但是,推荐第一种形式。在这种写法中,流迭代器的使用与其他迭代器的使用保持一致。如果向将次循环改为操作其他迭代器类型,修改起来非常容易。

也可以通过调用 copy 来打印 vec 中的元素,这比编写循环更为简单:

copy(vec,begin(), vec.end(), out_iter);

4.3 反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器会移动到前一个元素;递减一个迭代器会移动到下一个元素。

除了 forward_list 之外,其他容器都支持反向迭代器。我们可以通过调用 rbeign、rend、crbegin 和 crend 成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有 const 和非 const 版本。

注: 流迭代器不支持递减运算,因为不可能再有给流中反向移动。因此,不可能从一个 forward_list 或一个流迭代器创建反向迭代器。

base 成员函数

如果希望打印一串单词中的最后一个单词,可以使用反向迭代器

// 在一个都好分割的列表中查找最后一个元素
auto rcomma = find(line.crbeging(), line.crend(), ',');

假如我们的输入是:FIRST, MIDDLE, LAST,当我们试图打印找到的单词时,

// 错误:将逆序输出单词的字符,
cout << string(line.crbegin(), rcomma) << endl;.

这条语句会打印成 TSAL。这是因为我们使用的是反向迭代器,会反向处理 string。因此,上述输出语句从 crbegin 开始反向打印 line 中内容。而我们希望按正常顺序从 rcomma 开始到 line 末尾的字符。但我们不能直接使用 rcomma,因为它是一个反向迭代器,以为着它会反向朝着 string 开始的位置移动。我们需要做的是,将 rcomma 转换回一个普通迭代器,我们可以通过调用 reverse_iterator 中的 base 成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器

cout << string(rcomma.base(), line.cend()) << endl;

注: 反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素。


5 泛型算法结构

任何算法的最基本的特性是它要求其迭代器提供哪些操作。某些算法,如 find,只要求通过迭代器访问元素、递增迭代器以及比较两个迭代器是否相等这些能力。其他一些算法,如 sort,还要求读、写和随机访问元素的能力。算法所要求的迭代器操作可以分为 5 个迭代器类别,

  • 输入迭代器:只读,不写;单遍扫描,只能递增
  • 输出迭代器:只写,不读;单遍扫描,只能递增
  • 前向迭代器:可读写;多遍扫描,只能递增
  • 双向迭代器:可读写;多遍扫描,可递增递减
  • 随机访问迭代器:可读写;多遍扫描,支持全部迭代器运算

5.1 五类迭代器

类似容器,迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。例如,ostream_iterator 只支持递增、解引用和赋值。 vector、string 和 deque 的迭代器除了这些操作外,还支持递减、关系和算术运算。

迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。

C++ 标准指明了泛型和数值算法的每个迭代器参数的最小类别。例如,find 算法在一个序列上进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。replace 函数需要一对迭代器,至少是前向迭代器。类似的,replace_copy 的前两个迭代器参数也要求至少是前向迭代器。其第三个迭代器表示目的位置,必须至少是输出迭代器。其他的例子类似。对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误。

注: 对于向一个算法传递错误类别的迭代器的问题,很多编译器不会给出任何警告或提示。

迭代器类别

输入迭代器:可以读取序列中的元素。一个输入迭代器必须支持

  • 用于比较两个迭代器的相等和不相等运算符(==、!=)
  • 用于推进迭代器的前置和后置递增运算(++)
  • 用于读取元素的解引用运算符(*),解引用只会出现在赋值运算符的右侧
  • 箭头运算符(->), 等价于 (*it).member,即,解引用迭代器,并提取对象的成员

输入迭代器只用于顺序访问。对于一个输入迭代器,*it++ 保证是有效的,但递增它可能导致所有其他指向流的迭代器失效。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素。因此,输入迭代器只能用于单遍扫描算法。算法 find 和 accumulate 要求输入迭代器;而 istream_iterator 是一种输入迭代器。


输出迭代器:可以看作输入迭代器功能上的补集,只写而不读元素。输出迭代器必须支持

  • 用于推进迭代器的前置和后置递增运算(++)
  • 解引用运算符(*), 只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)

我们只能向一个输出迭代器赋值一次。类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。例如,copy 函数的第三个参数就是输出迭代器。ostream_iterator 类型也是输出迭代器。


前向迭代器:可以读写元素。这类迭代器只能在序列中沿一个方向移动。 前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此,我们可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。算法 replace 要求前向迭代器,forward_list 上的迭代器是前向迭代器。

双向迭代器:可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(–)。算法 reverse 要求双向迭代器,除了 forward_list 之外,其他标准库都提供符合双向迭代器要求的迭代器。

随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。 此类迭代器支持双向迭代器的所有功能

  • 用于比较两个迭代器相对位置的关系运算符(<、<=、> 和 >=)
  • 迭代器和一个整数值的加减运算(+、+=、-和= ), 计算结果是迭代器在序列中前进 (或后退)给定整数个元素后的位置
  • 用于两个迭代器上的减法运算符(-), 得到两个迭代器的距离
  • 下标运算符 (iter [n]) 与 *(iter [n]) 等价,算法 sort 要求随机访问迭代器。array、deque、string 和 vector 的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。

5.2 算法形参模式

在任何其他算法分类之上,还有一组参数规范。大多数算法具有如下 4 种形式之一:

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

其中 alg 是算法的名字,beg 和 end 表示算法所操作的输入范围。几乎所有算法都接受一个输入范围,是否有其他参数依赖于要执行的操作。这里列出了常见的一种,dest、 beg2 和 end2,都是迭代器参数。顾名思义,如果用到了这些迭代器参数,它们分别承担指定目的位置和第二个范围的角色。除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。

接受单个目标迭代器的算法

dest 参数是一个表示算法可以写入的目的位置的迭代器。算法假定:按其需要写入数据,不管写入多少个元素都是安全的。
注: 向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。

如果 dest 是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。更常见的情况是,dest 被绑定到一个插入迭代器或是一个 ostream_iterator 。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator 会将数据写入到一个输出流, 同样不管要写入多少个元素都没有问题。

接受第二个输入序列的算法

接受单独的 beg2 或是接受 beg2 和 end2 的算法用这些迭代器表示第二个输入范围。 这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。

如果一个算法接受 beg2 和 end2,这两个迭代器表示第二个范围。这类算法接受两个完整指定的范围:[beg, end)表示的范围和[beg2 end2)表示的第二个范围。

只接受单独的 beg2 的算法将 beg2 作为第二个输入范围中的首元素。 此范围的结束位置未指定,这些算法假定从 beg2 开始的范围与 beg 和 end 所表示的范围至少一样大

5.3 算法命名规范

一些算法使用重载形式传递一个谓词

接受谓词参数来代替 < 或 = 运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个额外谓词参数,来代替 < 或 =:

unique (beg, end);  // 使用 == 运算符比较元素
unique (beg, end, comp);  // 使用 comp 比较元素

两个调用都重新整理给定序列,将相邻的重复元素删除。第一个调用使用元素类型的 == 运算符来检查重复元素;第二个则调用 comp 来确定两个元素是否相等。由于两个版本的函数在参数个数上不相等,因此具体应该调用哪个版本不会产生歧义。

_if 版本的算法

接受一个元素值的算法通常有另一个不同名的(不是重载的)版本,该版本接受一个谓词代替元素值。接受谓词参数的算法都有附加的前缀:

find(beg, end, val);   // 查找输入范围中 val 第一次出现的位置
find_if(beg, end, pred);  // 查找第一个令 pred 为真的元素

这两个算法都在输入范围中查找特定元素第一次出现的位置。算法 find 查找一个指定值;算法 find_if 查找使得 pred 返回非零值的元素。

这两个算法提供了命名上差异的版本,而非重载版本,因为两个版本的算法都接受相同数目的参数。因此可能产生重载歧义,虽然很罕见,但为了避免任何可能的歧义,标准库选择提供不同名字的版本而不是重载。

区分拷贝元素的版本和不拷贝的版本

默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。如我们所见,写到额外目的空间的算法都在名字后面附加一个 _copy

reverse(beg, end);  // 反转输入元素中的元素顺序
reverse_copy(beg, end, dest);  // 将元素按逆序拷贝到 dest

一些算法同时提供 _copy 和 _if 版本。这些版本接受一个目的位置迭代器和一个谓词

// 从 v1 中删除奇数元素
remove_if(v1.begin(), v1.end(),
	[](int i) { return i % 2; }
);
// 将偶数元素从 v1 拷贝到 v2; v1 不变
remove_if_copy(v1.begin(), v1.end(), back_inserter(v2),
	[](int i) { return i % 2; }
);

6 特定容器算法

与其他容器不同,链表类型 list 和 forward_list 定义了几个成员函数形式的算法。特别是,它们定义了独有的 sort、merge、remove、reverse 和 unique。通用版本的 sort 要求随机访问迭代器,因此不能用于 list 和 fordward_list,因为这两个类型分别提供双向迭代器和前向迭代器。

链表类型定义的其他算法的通用版本可以用于链表,但代价太高。这些算法需要交换输入序列中的元素。一个链表可以通过改变元素间的链接而不是真的交换它们的值来快速“交换”元素。因此,这些链表版本的算法的性能比对应的通用版本好得多。
注: 对于 list 和 fordward_list,应该优先使用成员函数版本的算法而不是通用算法。

操作含义
lst.merge(lst2)将 lst2 的元素 合并入 lst。使用 < 运算符
lst.merge(lst2, comp)将 lst2 的元素 合并入 lst。使用给定的比较操作
lst.remove(val)调用 erase 删除掉与给定值相等的元素
lst.remove_if(pred)调用 erase 删除掉令一元谓词为真的元素
lst.reverse()反转 lst 中元素的顺序
lst.sort()使用 < 排序
lst.sort(comp)使用给定比较操作排序
lst.unique()调用 erase 删除同一个值的连续拷贝。使用 ==
lst.unique(pred)调用 erase 删除同一个值的连续拷贝。使用给定的二元谓词

注: 这些操作都返回 void。

splice 成员

链表类型还定义了 splice 算法,此算法是链表数据结构所特有的, 因此不需要通用版本。

操作含义
lst.splice(p, lst2)p 是一个指向 lst 中元素的迭代器,或一个指向 flst 首前位置的迭代器。函数将 lst2 的所有元素移动到 lst 中 p 之前的位置或是 flst 中 p 之后的位置。lst2 的类型必须与 lst 或 flst 相同,且不能是同一个链表
lst.splice(p, lst2, p2)p2 是一个指向 lst2 中位置的有效的迭代器。将 p2 指向的元素移动到 lst 中,或将 p2 之后的元素移动到 flst 中。lst2 可以是与 lst 或 flst 相同的链表。
lst.splice(p, lst2, b, e)b 和 e 必须表示 lst2 中合法的范围。将给定范围中的元素从 lst2 移动到 lst 或 flst。lst2 与 lst(或 flst)可以是相同的链表,但 p 不能指向给定范围中的元素。

注: lst.splice 或 flst.splice_after。

链表特有的操作会改变容器

多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如,remove 的链表版本会删除指定的元素。unique 的链表版本会删除第二个和后继的重复元素。

类似的,merge 和 splice 会销毁其参数。例如,通用版本的 merge 将合并的序列写到一个给定的目的迭代器;两个输入序列是不变的。而链表版本的 merge 函数会销毁给定的链表。即,元素从参数指定的链表中删除,被合并到调用 merge 的链表对象中。在 merge 之后,来自两个链表中的元素仍然存在,但它们都已在同一个链表中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值