Skip to content

Latest commit

 

History

History
801 lines (585 loc) · 56.1 KB

CH10_Generic_Algorithms.md

File metadata and controls

801 lines (585 loc) · 56.1 KB

标准库容器定义了相当少的操作,与其添加无数的功能到每一个容器中,标准库提供了一系列算法,其中大部分独立于任何特定的容器类型。这些算法是通用的(generic):它们在不同类型的容器和元素上进行操作。

本章的内容主要包括通用算法(generci algorithms)和更加详细的介绍迭代器。

顺序容器定义了简约的操作:其中大部分用于添加移除元素,访问首尾元素,判断容器是否为空,以及获取首元素和尾后元素的迭代器。

我们可以想象还需要其它有用的操作:查找特定的元素,替换或移除特定值,重排序容器中的元素。

与其将这些操作定义为每个容器类型的成员,标准库定义了一系列通用算法:“算法”意思是它们实现了常见的经典算法如排序和搜索,“通用”是由于它们操作与不同类型的元素以及跨越多种容器类型——不仅仅是标准库中的类型如 vector 和 list,还包括内置数组类型以及其它自定义的序列(sequences)类型。

10.1 概述

绝大部分算法定义在 algorithm 头文件中。标准库还在 numeric 头文件中定义了一小撮通用数字算法(generic numeric algorithms)。

通常来说,算法并不直接在容器上进行工作,而是通过遍历由两个迭代器组成的元素范围进行操作。当算法遍历整个范围时,它会对每个元素做一些事情。如要查找容器中的特定元素值最简单的方式是调用 find 算法:

int val = 42;
auto result = find(vec.cbegin(), vec.cend(), val);

find 的前两个参数是两个迭代器,它们组成了一个左包含的元素范围,第三个参数是一个值。find 将给定范围中的每个元素与待查找的值进行比较。它返回第一个等于这个值的第一个元素的迭代器。如果没有找到,find 将返回其第二个参数来表示失败。因而,我们可以通过比较返回值是否与第二个迭代器参数相等来判断是否找到。

由于 find 仅对迭代器操作,可以将 find 用于任何的容器类型。如将 find 用于 list<string>

string val = "a value";
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 来获取 ia 的首元素指针和尾后元素指针。

通过传递子范围的首元素和尾后元素的迭代器(或指针)从而仅对容器的子范围进行查找。如:

// 搜索元素从 a[1] 开始直到但不包括 ia[4]
auto result = find(ia+1, ia+4, val);

算法如何工作

通过考察 find 算法来了解算法是如何运用于不同的容器类型的。find 可以在未排序的一系列元素中查找特定的元素。以下是它采取的步骤:

  1. 访问序列的首元素;
  2. 将其与给定值进行比较;
  3. 如果这个元素匹配给定的值,find 将返回迭代器或者指针来标识这个元素;
  4. 否则 find 继续查找下一个元素,重复步骤 2 和 3;
  5. find 必须在到达序列的末尾时结束;
  6. 如果 find 到了序列的末尾,需要返回一个值来表示元素没有找到。返回的值需要与第 3 步中的类型相兼容;

以上所有操作都没有依赖于容器的类型,只要能够使用迭代器来访问元素,find 根本不需要依赖于容器的类型。

迭代器使得算法与容器互相独立

除了第二步之外的所有步骤都可以通过迭代器进行操作:迭代器解引用可以访问元素;如果找到了匹配的元素,find 将返回那个元素的迭代器;迭代器的自增操作将移动到下一个元素;“尾后”迭代器表示 find 已经到达了给定序列的尾部;find 返回尾后迭代器用于表示没有找到指定的值;

但是算法依赖于元素类型的操作

尽管迭代器使得算法和容器类型相互独立,大部分的算法使用一个或多个元素类型的操作。如:第 2 步用元素类型的 == 操作符比较每个元素和给定值。

其它的算法需要元素类型由 < 操作符。然而,绝大部分的算法还提供了一种方式允许我们提供自己的操作用于替换默认的行为。

关键概念:算法永不执行容器的操作

通用算法自己不会执行容器的操作。它们只会操作迭代器。算法不直接调用容器的成员函数有一个重要的隐喻:算法永远不会改变底层容器的长度。算法可能会改变元素的值,可能会在容器中移动元素,但是它们不会直接添加或移除元素。

我们将在后面看到,有一种特殊类别的迭代器——插入器(inserter),除了遍历序列之外。当我们给这个迭代器赋值时,它将在底层容器中执行插入操作。当算法操作在这种迭代器上时,迭代器具有加元素到容器中的效果。但是算法本身不会插入元素,甚至它都不知道插入元素这回事。

10.2 算法入门

标准库提供了超过 100 个算法。幸运的是算法有固定的结构。理解结构使得学习和使用算法比记住 100 个以上的算法要容易。在本章,我们将描述如何使用算法,并且讲解它们的组织原则。附录 A 中按照算法的操作进行分类列举。

除了一些小的例外,算法操作于一组元素。我们称这个范围为“输入范围”(input range)。带输入范围的算法总是用其前两个参数来表示范围。这两个参数分别表示要处理的首元素和尾后元素。

尽管绝大部分算法都处理一组输入范围,它们操作元素的方式不一样。理解算法的最基本的方法是直到它们是读元素、写元素还是对元素进行重排序。

10.2.1 只读算法

有很多算法对输入范围只读不写。find 和 count 算法就是一个例子。另外一个只读算法是 accumulate,它定义在 numeric 头文件中。accumulate 函数有三个参数,前两个指定输入范围,第三个参数是综合的初始值。假设 vec 是一个整数序列,那么:

int sum = accumulate(vec.cbegin(), vec.cend(), 0);

第三个参数的类型决定了使用哪个加操作,并且是 accumulate 的返回值类型。

算法和元素类型

accumulate 使用其第三个参数作为求和的起点有一个重要的隐喻:元素类型必须与总和的类型可以相加。也就是说元素的类型必须与第三个参数的类型相匹配或者可以相互转换。在这个例子中 vec 中的元素类型可以是 int,double,long long 或者其它可以与 int 相加的类型。

另外一个例子是 string 有加操作符,可以通过调用 accumulate 将 vector 中的元素拼接起来:

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

这个调用 v 中的每个元素拼接到一个 string 上,这个 string 最开始是空字符串,请注意这里需要显示创建 string 作为第三个参数,如果传递的是空字符串字面量将会是编译时错误:

// error: const char* 上没有加操作符
string sum = accumulate(v.cbegin(), v.cend(), "");

如果传递 string 字面量,存储 sum 的对象类型将是 const char*,这个类型决定了使用哪个加号操作符。由于 const char* 上没有加操作符,这个调用将无法通过编译。

通常在只读算法上最好使用 cbegin() 和 cend(),如果打算使用返回的迭代器来改变元素的值就要传递 begin() 和 end() 。

在两个序列上进行操作的算法

另外一个只读算法是 equal,用于判断是否两个序列中的值完全一样。它将第一个序列中的每个元素与第二个序列中对应位置的元素进行比较,如果所有的对应位置的元素都相等,返回 true,否则返回 false。这个算法有三个迭代器:前两个表示第一个序列的范围,首元素和尾后迭代器;第三个是第二个序列的首元素:

// roster2 至少要有与 roster1 一样多的元素
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

由于 equal 在是迭代器上定义的,可以将 equal 用于比较不同类型的容器中的元素。甚至,元素类型也不需要完全一致,只要可以使用 == 进行元素值比较就行。如,roster1 可以是 vector<string> ,roster2 可以是 list<const char*>

然而,equal 做出了一个严格的假设:它假设第二个序列至少跟第一个序列一样长。这个算法潜在地会遍历第一个序列中的所有元素。它假设第二个序列中一定会有对应的元素。

**警告:**以单个迭代器表示第二个序列的算法会假设第二个序列至少跟第一个序列一样长。

10.2.2 写容器元素的算法

一些算法会给序列中的元素赋予新值。当使用会对元素赋值的算法时需要注意,必须保证算法写入的序列其大小大于等于需要写入的数目。记住,算法不会直接调用任何容器的操作,所以它们本身没有任何办法改变容器的大小。

其中一部分算法只会对输入范围内的元素进行写入,它们没有上面谈到的危险,原因是它们只会对输入范围内的元素进行写入。

比如,fill 以一对迭代器表示范围,以及第三个参数表示要写入的值。fill 将给定值赋值给范围内的所有元素。如:

fill(vec.begin(), vec.end(), 0);
fill(vec.begin(), vec.begin() + vec.size()/2, 10);

由于 fill 只会往给定的输入序列中写入值,只要传入的是合法的输入序列,那么写入将是安全的。

关键概念:迭代器参数

有些算法同时从两个序列中读取元素,构成这些序列的的元素可以被存储在不同类型的容器中。如第一个序列可能被存储在 vector 中,第二个序列则存储在 list 或者 deque ,内置数组或者别的类型中。甚至,两个列中的元素类型都不需要完全匹配。只要能够比较两个序列中的元素。比如,在 equal 算法中,元素类型不需要完全一致,但是需要可以使用 == 来比较两个序列中的元素。

操作两个序列的算法的不同之处在于如何传递第二个序列。有一些算法比如 equal,有三个迭代器:前两个表示第一个序列的范围,第三个迭代器表示第二序列的首元素。其它的算法以四个迭代器为参数:分别表示两个序列的范围。

使用一个迭代器表示第二个序列的算法假设第二个序列至少有第一个序列那么长。由程序员保证算法不会访问第二个序列中不存在的元素。比如 equal 有可能会将第一个序列中的每个元素与第二序列中的对应位置的元素进行比较。如果第二个序列是第一个序列的子序列,那么将会产生很严重的错误 —— equal 将会访问超出第二个序列尾部的元素。

算法不会检查输出操作

有些算法只有一个单独的迭代器表示写入目标。这些算法将新值赋值到以目标迭代器(destination iterator)表示的元素开始的序列的元素中。 fill_n 以一个迭代器,计数器和值作为参数。它将这个给定值赋值到从迭代器所表示的元素开始的指定数目的元素中去。如:

vecotr<int> vec;
fill_n(vec.begin(), vec.size(), 0);

fill_n 假设写入指定数目的元素是安全的。这是因为在调用 fill_n(dest, n, val) 中,fill_n 假设 dest 表示一个元素,并且从 dest 开始至少有 n 个元素。

对于初学者来说,在空容器上调用 fill_n (以及其它写入的算法)是一个常见的错误:

vector<int> vec;
// 试图写入 10 个不存在的元素是严重的错误
fill_n(vec.begin(), 10, 0);

这个调用想要写入 10 个元素,但是容器中根本没有元素。结果是未定义的。

警告: 写入到目标迭代器的算法假设目标序列足以容纳将要写入的元素。

介绍 back_inserter

有一种保证算法有足够的元素用以写入,那就是使用插入迭代器(insert iterator)。插入迭代器是往容器中添加元素的迭代器。平常,当我们给迭代器赋值时,我们是给迭代器所表示的元素赋值。当我们给插入迭代器赋值时,一个等于右边值的新元素被添加到容器中。

插入迭代器将在后面更为详细的介绍,现在我们先使用 back_inserter 来举几个例子,back_inserter 是定义在 iterator 头文件中的函数。

back_inserter 以容器引用为参数,返回一个绑定到容器上的插入迭代器。当通过这个迭代器给元素赋值时,将调用容器的 push_back 函数给容器添加元素。如:

vector<int> vec;
auto it = back_inserter(vec);
*it = 42;

我们经常用 back_inserter 创建的迭代器用作算法的目的迭代器。如:

vector<int> vec;
fill_n(back_inserter(vec), 10, 0);

在每次迭代时,fill_n 给序列中的元素赋值,由于我们传入的是 back_inserter 的返回值,每次赋值都将调用 vec 的 push_back ,因而每次赋值 fill_n 都会添加一个元素到容器的尾部。

拷贝算法

copy 算法是另一个将值写入到由目的迭代器表示的输出序列中的例子。这个算法只有三个参数。前两个表示输入序列;第三个表示输出序列的首元素。这个算法将输入范围内的元素拷贝输出序列中。有一点很重要的是传递给 copy 的输出序列至少要和输入序列一样长。如将值拷贝内置数组中去:

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret = copy(begin(a1), end(a1), a2);

copy 的返回值是自增后的目的迭代器。意味着 ret 将指向 a2 的尾后位置。

有多个算法提供了“拷贝”版本,相比于将计算后的值重新存入输入序列中,这个算法创建一个新的序列以容纳结果。比如 replace 算法接收四个参数:两个迭代器表示输入序列,以及两个值。它将序列中的每个等于第一个值的元素替换为第二个值。如:

replace(ilist.begin(), ilist.end(), 0, 42);

上面的调用将所有的 0 替换为 42。如果想要保持原始的容器不变的话,需要调用 replace_copy ,这个算法有第三个迭代器参数表示输出目的地。如:

replace_copy(ilist.cbegin(), ilist.cend(), back_inserter(ivec), 0, 42);

在此调用之后 ilist 将保持不变,而 ivec 的元素将是 ilist 拷贝,并将其中所有的 0 替换为 42。

10.2.3 对容器元素进行重排序的算法

有些算法对容器中的元素进行重排序,一个显著的例子就是 sort 算法。调用 sort 将使用元素的 < 操作符将输入范围内的元素排序。

消除元素

一个例子是消除容器中的相同的字符串。首先对这些字符串进行排序,然后调用 unique 将所有唯一的字符串放到容器的首部,并返回最后一个唯一字符串的下一个位置的迭代器。unique 本身是不改变容器的大小的,所以需要用容器的 erase 成员移除元素:

void elimDups(vector<string> &words)
{
    sort(words.begin(), words.end());
    auto end_unique = unique(words.begin(), words.end());
    words.erase(end_unique, words.end());
}

调用完 unique 之后,返回值迭代器后面的值到底是什么我们无法知道,它可能已经被算法给改写了。

由于标准库算法是在迭代器而不是容器上进行操作,算法不能直接添加或移除元素。

使用容器操作来移除元素

为了移除无用的元素,必须使用容器的操作。

10.3 定制操作

大多数算法会比较容器中的元素。默认情况下算法使用的是 <== 操作符。算法还定义了允许我们提供自己的操作来替换默认操作符的版本。

比如对于 sort 算法使用的是 < 操作符。可能我们的序列并不是按照 < 进行排序的,或者有些类型根本就没有 < 操作符,在这两种情况下都需要重载默认的 sort 行为。

10.3.1 传递函数给算法

对于字符串序列进行排序可以是先按照长度进行排序,对于长度一样的再按照字典顺序进行排序。为了这样做需要使用第二个版本的 sort,这个版本的 sort 有第三个参数称之为谓词(predicate)。

谓词

谓词是一个可以被调用然后返回一个值的表达式,返回值可以作为条件使用。标准库使用的谓词可以是一元谓词(unary predicate)(意思是只有一个参数)也可以是二元谓词(binary predicate)(意思是有两个参数)。带有谓词的算法在输入范围内的元素上调用这个谓词。因而,必须要可以将元素类型转为谓词的参数类型。

使用二元谓词的 sort 版本将给定谓词替换 < 操作用于比较元素。提供给 sort 算法的谓词必须满足 §11.2.2 中描述的限制,现在我们只需要知道这个操作必须在每一个元素之间提供稳定的顺序。单纯比较字符串的长度就是其中一个例子。如:

bool isShorter(const string &s1, const string &s2) {
    return s1.size() < s2.size();
}
sort(words.begin(), words.end(), isShorter);

这样所有的字符串就是按照长度进行排序的了。

排序算法

当使用长度进行排序时,我们依然希望保持相同长度的元素之间的字典排序。为了达到这样的效果,我们将使用 stable_sort 算法。稳定排序(stable sort)将保持原来相等的元素之间的顺序。

通常,我们并不关心相等元素之间的相对顺序,毕竟它们都是相等的。然而,现在的情况是我们定义相等是按照长度来定义的。相同长度的字符串的内容依然是有区别的。通过调用 stable_sort 可以保持相同长度的字符串的字典顺序:

elimDups(words);
stable_sort(words.begin(), words.end(), isShorter);

10.3.2 lambda 表达式

传递给算法的谓词必须有一个或两个参数。但是有时我们想传递多于算法的谓词需要的参数。为了查找字符串序列中大于等于给定长度的字符串,我们写了一个函数:

void biggies(vector<string> &words, vector<string>::size_type sz)
{
    elimDups(words);
    stable_sort(words.begin(), words.end(), isShorter);
    auto wc = find_if(words.begin(), words.end(), [sz](const string &a) {
        return a.size() >= sz;
    });
}

我们使用 find_if 标准库算法来查找一个元素大于等于给定长度的。find_if 接收一对迭代器表示输入范围,不同于 find 的是它接收的第三个参数是谓词。find_if 算法在每个元素上调用给定的谓词。它将返回第一个使得谓词的返回值为非 0 值的元素迭代器,或者当无法找到这样的元素时返回尾部迭代器。

由于 find_if 接收的是一元谓词,任何传递给 find_if 的函数必须只有一个参数。所以想要传递可变的 size 参数给谓词必须使用 lambda 表达式。

介绍 lambda

我们可以传递任何可调用对象(callable object)给算法。如果可以给对象或表达式运用调用操作符,它就是可调用的(callable)。也就是说如果 e 是可调用的表达式,那么可以写作 e(args) 其中 args 是一个逗号分隔的零个或多个参数的列表。

目前我们使用的可调用对象就是函数和函数指针。还有两种可调用对象:重载了调用操作符的类将在 §14.8 中介绍,以及现在将要介绍的 lambda 表达式(lambda expressions)。

lambda 表达式表示可调用的代码单元。可以被视作匿名内联函数。与任何函数一样,lambda 有返回类型,参数列表和函数体。与函数不同的是,lambda 可以被定义在函数中。其有如下形式:

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

其中捕获列表(capture list)(经常是空的)表示定义在外围函数中的一系列本地变量。返回值,参数列表和函数则与普通函数是一样的。然而,与普通函数不同的是,lambda 必须使用尾部返回(trailing return)来表示返回类型。

可以忽略参数列表和返回类型,但是必须总是包含捕获列表和函数体:

auto f = [] { return 42; };

我们将 f 定义为一个不接受任何参数返回 42 的可调用对象。使用 lambda 的方式与任何函数调用都是一样的。如:

cout << f() << endl;

省略 lambda 中的括号和参数列表表示其参数列表为空。如果省略返回值类型,lambda 通过函数体中语句进行推断。如果函数体中只有一个 return 语句,那么返回类型就是被返回的表达式的类型。否则,返回类型就是 void。

注意:lambda 中的函数体如果包含了除了返回语句之外的任何语句,都将被推断为返回 void。

传递参数给 lambda

与常规的函数调用一样,lambda 调用中的实参也是用于初始化其形参的。实参和形参的类型必须匹配。与常规函数不同的是,lambda 没有默认实参。因而,调用 lambda 给足实参。一旦形参被初始化,函数体就开始执行。

如下 lambda 需要传递参数:

[](const string &a, const string &b) {
    return a.size() < b.size();
}

空的捕获列表表示不使用任何外围函数的本地变量。lambda 的参数与常规函数的参数一样是 const string 的引用。我们可以重写 stable_sort 的调用从而使用 lambda:

stable_sort(words.begin(), words.end(),
    [](const string &a, const string &b) {
        return a.size() < b.size();
    });

stable_sort 需要比较元素值时,它将调用给定的 lambda 表达式。

使用捕获列表

注意上面使用的 find_if 中的 lambda 表达式中的捕获列表中的 sz。尽管 lambda 被定义在函数中,只有被指定了想要使用的外围函数本地变量才能使用。lambda 通过捕获列表来指定想要使用的变量。捕获列表中包含了 lambda 用于访问外围函数本地变量的信息。

这样 find_if 使用的 lambda 表达式捕获了 sz,并且只有一个参数,函数体中将给定 string 的长度与捕获值 sz 进行比较:

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

[] 中是逗号分隔的捕获列表,里边的名字是外围函数中定义的本地变量。由于这个 lambda 捕获了 sz,函数体就可以使用 sz,而没有捕获 words 就不能访问此变量。

注意:lambda 只能使用出现在捕获列表中的外围函数本地变量。

for_each 算法

for_each 算法有一个可调用对象并在输入范围内的每个元素上调用此对象。如:

for_each(wc, words.end(), [](const string &s) { cout << s << " "; });

注意:这里并没有捕获 cout,捕获列表只用于捕获外围函数中定义的非静态变量。lambda 可以自由使用定义在函数外的变量,此处 cout 并不是一个本地变量;这个名字定义在 iostream 头文件中。只要包含了 iostream 头文件,这个 lambda 就可以使用 cout 。

捕获列表只用于捕获外围函数中定义的非静态变量;lambda 可以自由使用本地 static 变量或者定义在函数体外的变量。

10.3.3 lambda 捕获和返回

当我们定义 lambda 时,编译器为此 lambda 产生一个匿名的新类类型。在 14.8.1 节我们将看到这个类是怎样的。现在只需要知道当我们定义一个 lambda 时,我们同时定义了一个新类型和这个类型的对象:这个由编译器生成的类的匿名对象。

默认情况下,从 lambda 中生成的类包含有数据成员,这些数据成员与捕获列表中的变量一一对应。当 lambda 对象创建时其数据成员被初始化。

值捕获

与参数传递一样,可以通过值或引用捕获变量。目前我们使用到的是值捕获。值捕获中的变量必须是可拷贝的。与参数不同的是,捕获变量是在 lambda 创建时拷贝的,而不是在调用时:

void fcn1()
{
    size_t v1 = 42;
    auto f = [v1] {return v1;};
    v1 = 0;
    auto j = f(); // j 是 42;在创建 f 是拷贝了 v1 的值;
}

由于在创建 lambda 时已经拷贝了值,因而接下来对捕获变量的修改不会影响 lambda 中对应的值。

以下是所有的捕获方式:

  • [] 空的捕获列表。lambda 不适用外围函数中的变量;j
  • [names] names 是逗号分隔的捕获列表。默认情况下,变量是值捕获的。在名字前加上 & 就是引用捕获;
  • [&] 隐式引用捕获列表。在 lambda 函数体中所使用的外围函数的本地变量都被隐式地引用捕获;
  • [=] 隐式值捕获列表。在 lambda 函数体中所使用的外围函数本地变量都被隐式地值捕获;
  • [&,identifier_list] identifier_list 是逗号分隔的外围函数本地变量列表,这些变量是值捕获的,并且不能在名字前加上 &,其它的使用到外围本地变量是隐式引用捕获的;
  • [=,reference_list] reference_list 是逗号分隔的外围函数的本地变量引用列表,这些变量是引用捕获的,所以必须在名字前加上 &,这个列表不能包括 this 指针。其它使用到的外围本地变量都是隐式值捕获的;

引用捕获

可以定义以引用捕获变量的 lambda,如:

void fcn2()
{
    size_t v1 = 42;
    auto f2 = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();
}

v1 前的 & 表示 v1 是引用捕获的。被引用捕获的变量与其它引用是一样的。当在 lambda 函数体中使用引用捕获的变量时,使用的是这个引用绑定的对象。这个例子中返回的 v1 其实是其绑定的外围函数中的本地变量的值。

使用引用捕获的变量跟常规的使用引用变量一样,当 lambda 被调用时需要确保引用捕获的变量确实存在。被 lambda 捕获的变量是本地变量,这些变量在函数完成时将消失。如果在函数结束后还可以调用 lambda ,那么将是严重的编程错误。

引用捕获有时是必须的,比如不可复制的对象:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ')
{
    for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });
}

我们可以从函数中返回 lambda ,函数可以直接返回可调用对象或者函数返回一个具有可调用对象数据成员类对象。如果函数返回 lambda ,那么与函数不能返回本地变量的引用一样,这个 lambda 一定不能包含引用捕获。

警告:当我们进行引用捕获时,一定要保证当 lambda 执行时变量是存在的。

建议:保持 lambda 的捕获尽量的简单

lambda 捕获的信息在创建时保存,在执行时进行访问。保证这些信息的可用性是程序员自己的责任。按值的方式捕获 int ,string 和其它非指针变量是很简单的。这个时候,我们只需要关心在捕获时变量是否有我们需要的值。

如果捕获指针或迭代器或者按引用捕获,我们需要保证绑定到迭代器,指针和引用的对象在 lambda 执行时依然存在。甚至需要保证这个对象值是合理的。在 lambda 创建和执行之间的代码可能会改变按引用捕获的对象值。可能在创建时的值的确是我们想要的,但是当执行时其值已经变得很不一样了。

为了避免由捕获带来的问题尽可能缩小捕获的变量,并且尽可能避免捕获指针,迭代器和引用。

隐式捕获

相比于显式列举需要捕获的变量,可以让编译器对 lambda 函数体的代码进行推断。为了引导编译去推断捕获列表,我们在捕获列表中使用 &=,其中 & 告知编译器按引用捕获,= 告知编译按值捕获。如以下是重写的 find_if

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

如果想让其中的一些变量按值捕获,可以昏庸隐式和显式捕获:

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;
});

Mutable Lambdas

默认情况下,lambda 不会改变按值捕获的变量值,如果想要改变捕获变量的值,必须在参数列表后加上关键词 mutable。有 mutable 关键词的 lambda 不能省略参数列表:

void fcn3()
{
    size_t v1 = 42;
    auto f = [v1] () mutable { return ++v1; };
    v1 = 0;
    auto j = f();
}

按引用捕获的对象是否可以改变仅仅依赖于引用的对象是 const 还是非 const 的:

void fcn4()
{
    size_t v1 = 42;
    auto f2 = [&v1] { return ++v1; };
    v1 = 0;
    auto j = f2();
}

指定 lambda 的返回类型

目前我们所些的 lambda 只有一个 return 语句。因而我们不需要指定返回类型。如果 lambda 函数体中包含了任何不是 return 语句的话,那么 lambda 将被推断为返回 void。推断为返回 void 的 lambda 不会返回任何值。如:

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

transform 将输入序列中的每个元素通过调用 lambda 进行转换,并将结果存储到其目的迭代器所表示的序列中。目的序列和输入序列可以是同一个。由于这里的 lambda 中语句多于一条,我们必须指定其返回值。

10.3.4 绑定实参

lambda 表达式对于只用于一两个地方的简单操作非常有用。如果我们需要将相同的操作用于多个地方的话,我们应该定义函数而不是重复相同的 lambda 表达式。同样,如果操作比较复杂的话,最好是定义函数。

对于没有捕获列表的 lambda 表达式很容器用函数来替换。然而,想用函数来替换捕获了本地变量的 lambda 就有难度了。如:

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

想将 check_size 函数用于 find_if 肯定是不行的,find_if 要求函数只能接收一个参数。为了能够传递 check_size 必须要进行参数绑定。

标准库 bind 函数

使用定义了 functional 头文件中的 bind 函数。bind 可以被认为是通用的函数适配器(function adaptor)。它接收一个可调用对象并产生一个新的可调用对象,其中改变了原始函数的参数列表。bind 的通用形式是:

auto newCallable = bind(callable, arg_list);

其中 newCallable 是一个可调用对象,arg_list 是逗号分隔的参数并与给定的 callable 中的参数一一对应。也就是说,当我们调用 newCallable 时,newCallable 将调用 callable,并传递参数列表 arg_list

参数列表 arg_list 可以包含形式为 _n 的名字,其中 n 是一个整数。这种形式的参数称之为 “占位符”(placeholder),表示 newCallable 的形参列表。意味着当我们调用 newCallable 并传递给其多个参数,这些参数会被填值到 _1 _2 _3 并以 arg_list 的顺序传递给 callable 可调用对象。传递给 newCallable 的参数个数与占位符的个数一样多。

将 sz 绑定到 check_size

以下是给 check_size 绑定一个固定的 sz 参数:

auto check6 = bind(check_size, _1, 6);

这个例子中 bind 只有一个占位符,意味着 check6 有一个参数。占位符出现在 arg_list 的第一个位置,意味着 check6 的参数对应于 check_size 的第一个参数。这个参数的类型是 const string&,意味着 check6 中的参数也是 const string&。那么当调用 check6 时就必须传递类型为 string 的参数,check6 会将其传递给 check_size 的第一个参数。

arg_list 中的第二个参数是值 6,这个值被绑定到 check_size 的第二个参数上,无论任何时候我们调用 check6,它都会将 6 传递给 check_sz 的第二个参数:

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

使用 bind 可以替换原来的 lambda 版本的 find_if

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

bind 的调用生成了一个新的可调用对象,并将 check_size 的第二个参数绑定到值 sz 上。当 find_if 调用此对象时将会用 string 和 sz 调用 check_size

使用 placeholders 名称空间

名字 _n 定义在名称空间 placeholders 中,这个名称空间本身被嵌套在 std 名称空间中。用 using 声明可以简写占位符的名字:using std::placeholders::_1;

如果嫌麻烦的话,就在函数中使用 using 指令(using directive):using namespace std::placeholders; ,using 指令将使得 std::placeholders 中的名字都可见,这样就不需要再使用完全限定名了。

与 bind 函数一样,placeholders 名称空间定义在 functional 头文件中。

bind 的参数

bind 函数可以用于固定参数的值。甚至,bind 还可以用于重排序参数。如:

auto g = bind(f, a, b, _2, c, _1);

将产生一个有两个参数的新的可调用对象 g。其中 g 的第一个参数被传递给 f 的第五个参数,g 的第二个参数将被传递给 f 的第四个参数。效果相当于 g(_1, _2)f(a, b, _2, c, _1) 一样,当调用 g(X, Y) 时与 f(a, b, Y, c, X) 效果一样。

还有 bind 可以对一个函数的参数进行重排序,如:

sort(words.begin(), words.end(), isShorter);
sort(words.begin(), words.end(), bind(isShorter, _2, _1));

在第一个调用中,sort 调用的正常的 isShorter ,其含义是较短的字符串在前面。第二个调用中,sort 调用的反向的 isShorter ,其含义是较长的字符串在前面。

绑定引用形参

通常调用 bind 时,非占位符参数时拷贝到生成的调用对象中的。然而有时我们希望这个参数是按引用绑定的。如:

ostream &print(ostream &os, const string &s, char c)
{
    return os << s << c;
}

由于 os 不能被拷贝,所以不能直接 bind 这个参数。如果想要以引用方式进行绑定需要调用 ref 函数:

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

ref 将返回一个对象其本身是可以被拷贝的,然而它的内部包含了给定参数对象的引用。同时还有一个 cref 函数用于生成一个类以容纳 const 引用。ref 和 cref 都定义在 functional 头文件中。

向后兼容:绑定参数

早期的 C++ 版本对于绑定参数到函数有诸多限制,并且更加复杂。标准库定义了 bind1st 和 bind2nd,在新标准下应该使用 bind 函数。

10.4 再谈迭代器

除了定义在每个容器中的迭代器,标准库还在 iterator 头文件中定义了几种别的迭代器:

  • 插入迭代器(Insert iterators):这些迭代器绑定到容器上,并且可用于往容器中插入元素;
  • 流迭代器(Stream iterators):这些迭代器绑定到输入输出流上,并可用于迭代相关的 IO 流;
  • 反向迭代器(Reverse iterators):这些迭代器向后移动而不是向前移动,除了 forward_list 之外都提供反向迭代器;
  • 移动迭代器(Move iterators):特殊的迭代器用于移动元素而不是拷贝;

10.4.1 插入迭代器

插入器(inserter)是一个迭代器适配器,以一个容器为参数,生成一个可以插入元素的迭代器。当通过插入迭代器赋值时,迭代器将调用容器的操作添加一个元素到指定的位置。这些迭代器支持的操作包括:

  • it = t 插入值到 it 所表示的当前位置,根据不同种类的插入迭代器,可能会调用容器的 push_backpush_front 或 insert 方法;
  • *it ++it it++ 这些操作存在当不做任何事情,每次都是返回 it 本身;

有三种不同种类的插入器。它们之间的区别在于元素插入到哪个位置:

  • back_inserter() 这个迭代器调用容器的 push_back
  • front_inserter() 这个迭代器调用容器的 push_front;
  • inserter() 这个迭代器调用 insert,inserter 有第二个参数,并且这个参数必须是容器中的迭代器。元素被插入到给定迭代器所表示的元素前面;

只有容器有 push_front 函数时才能使用 front_inserter ,同样只有容器有 push_back 时才能使用 back_inserter

需要理解的是当调用 inserter(c, iter) 时,如果连续向其赋值会把元素顺序的插入到这个迭代器所表示的元素之前。而 front_inserter 生成的迭代器的行为与 inserter 生成的迭代器有很大的不同。当使用 front_inserter 时,元素总是被插入到首元素之前,意味着后面插入的元素在前面插入的元素之前。而 inserter 生成的迭代器刚好相反,后面插入的元素在前面插入的元素之后。如:

list<int> lst = {1,2,3,4};
list<int> lst2, lst3;
// copy 完成后,lst2 的内容是 4 3 2 1
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
// copy 完成后,lst2 的内容是 1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));

10.4.2 iostream 迭代器

尽管 iostream 类型不是容器,依然可以在 IO 类型上使用迭代器。istream_iterator 读取输入流,ostream_iterator 写输出流。这些迭代器将其对应的流当作特定类型元素的序列。使用流迭代器,我们可以使用通用算法对流对象进行读写。

以下是 istream_iterator 的操作:

  • istream_iterator<T> in(is); in 从 is 中读取类型 T 的值;
  • istream_iterator<T> end; in 的尾后迭代器;
  • in1 == in2 in1 != in2 in1 和 in2 必须读取相同类型的值。它们只有都是 end 值或者绑定到同一个输入流上时才相等;
  • *in 返回从流中读取到值;
  • in->mem(*in).mem 含义相同,访问读取到的值的成员 mem;
  • ++in in++ 从流中读取下一个值,使用的操作符是元素成员的 >> 操作符。前置版本返回自增后的迭代器,后置版本返回旧的迭代器;

当创建流迭代器时,需要指定元素的类型。istream_iterator 使用 >> 读取流。因而这个元素类型必须要有 >> 操作符才行。在创建 istream_iterator 时我们可以将其绑定到一个流上,或者当默认初始化时,创建的迭代器被当作尾后值。如:

istream_iterator<int> int_it(cin);
istream_iterator<int> int_eof;
ifstream in("afile");
istream_iterator<string> str_it(in);

只有当要给迭代器绑定的流到达了文件尾部或者遇到了 IO 错误时才会等于 end 迭代器。以下是利用 istream_iterator 读取流中的数据并存储到 vector 中:

istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while (in_iter != eof)
    vec.push_back(*in_iter++);

以及用 istream_iterator 用于初始化 vector:

istream_iterator<int> in_iter(cin), eof;
vector<int> vec(in_iter, eof);

将流迭代器用于算法

由于算法是在迭代器上进行的,并且流迭代器支持一些迭代器操作。算法是按照迭代器分类决定哪些迭代器是可以用于此算法的。如 accumulate 可以用于一对 istream_iterator

istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;

istream_iterator 允许延迟计算

当将 istream_iterator 绑定到流时,并没有说会立即读取这个流。实现会推迟到使用迭代器时才读取流。标准库保证当第一次解引用迭代器前,流是被读取了的。对于大部分程序来说,是立即读取还是延迟读取是没有关系的。然而,如果在同一个流上同时绑定了两个迭代器,此时就要当心了。

以下是 ostream_iterator 的操作:

  • ostream_iterator<T> out(os); out 将类型 T 的值写入到输出流 os 中;
  • ostream_iterator<T> out(os, d); out 将类型 T 的值和 d 一起写入到输出流 os 中,d 是一个 C 风格字符串的指针,每次写入时 d 都在 T 值的前面;
  • out = val; 将 val 通过 out 写入到其绑定的输出流中,使用的 val 的 << 操作符。val 的类型必须与 out 可写的对象类型相兼容;
  • *out ++out out++ 这些操作存在但是没有做什么事,每个操作都返回 out;

ostream_iterator 可以定义在任何有 << 操作符的元素类型。创建 ostream_iterator 时可以提供第二参数是一个字符串,这个字符串必须是 C 风格字符串,它将在任何元素打印之前先打印。ostream_iterator 必须与特定的流绑定,并且没有尾后迭代器。

使用 ostream_iterator 写入一系列的值:

ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
    *out_iter++ = e;
cout << endl;

每次赋值给 out_iter 时都会提交一个写入操作。需要注意的是其实我们可以省略解引用和自增操作,这两个操作并没有什么作用。但是现在这种写法是更好的,原因是更符合习惯,并且可以很容器的替换为别的迭代器类型。

除了自己写循环之外,我们还可以调用 copy 算法,如:

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

10.4.3 反向迭代器

反向迭代器是一种反向遍历容器的迭代器。反向迭代器反转了自增和自减的含义。自增一个反向迭代器会将迭代器移动到前一个元素;自减则会将迭代器移动到下一个元素。

除了 forward_list 的容器都有反向迭代器。通过调用 rbegin,rend,crbegin 和 crend 成员函数来获取反向迭代器。这些成员返回指向最后一个元素和首前位置的迭代器。与正常的迭代器一样,反向迭代器分为 const 和非 const 的。

在反向迭代器上调用 sort 将容器中的元素按照相反的顺序排序。如:

sort(vec.begin(), vec.end());
sort(vec.rbegin(), vec.rend());

反向迭代器需要自减操作符

只有同时支持自减和自增操作符的迭代器才能定义反向迭代器。毕竟反向迭代器的目的就是为了让迭代器反向移动。除了 forward_list ,所有标准容器的迭代器都同时支持自减和自增操作。流迭代器不支持自减操作,毕竟它不能反向移动。

反向迭代器和其它迭代器之间的关系

reverse_iterator 有一个 base 成员,返回其对应的正常迭代器。反向迭代器的正常迭代器指向的位置与反向迭代器本身是不一样的,正常迭代器指向的位置是下一个位置。

10.5 通用算法的分类

算法的基本特性是它对于迭代器的要求。有些算法如 find 只需要能够通过迭代器进行元素访问、自增迭代以及比较两个迭代器相等性。有些算法如 sort 需要可以进行读、写和随机访问元素。算法要求的迭代器操作被分类为五种迭代器类别(iterator categories),每个算法都说明了其每个迭代器参数所属的类别:

  • 输入迭代器(Input iterator):读,但不写;单遍扫描(single-pass),只能递增;
  • 输出迭代器(Output iterator):写,但是不能读;单遍扫描,只能递增;
  • 前向迭代器(Forward iterator):可以读写;多遍扫描(multi-pass),只能递增;
  • 双向迭代器(Bidirectional iterator):可读写;多遍扫描;可递增递减;
  • 随机访问迭代器(Random-access iterator):可读写;多遍扫描,支持全部迭代器运算;

另一种对算法进行分类的方法是他们是否对元素进行读,写或者重排序。算法还共享一组参数传递规范和明明规范,这个会在后面介绍。

10.5.1 按照5种迭代器分类

与容器一样,迭代器定义一组公共操作。一些操作是所有迭代器都会提供的;其它一些操作则只有特定种类的迭代器会提供。如 ostream_iterator 只能进行递增,解引用和赋值。vector,string 和 deque 的迭代器除了支持这些操作外,还支持递减、关系和算术运算。

迭代器按照他们提供的操作进行分类,并且组成了某种形式的层级。除了输出迭代器,如果一个迭代器处于更高的层级那么它将提供低层级迭代器的所有操作。

标准说明了通用和数字算法对迭代器参数所要求的最低层级。如 find 最低要求需要输入迭代器。replace 函数需要一对迭代器至少是前向迭代器。同样,replace_copy 要求其前两个迭代器至少得是前向迭代器。其第三个迭代器至少得是输出迭代器。对于每个迭代器,至少得是要求的最低层级,传递更低层级的迭代器是一种错误。

警告:大多数迭代器在我们传递错误的迭代器类别时并不会识别这个错误。

迭代器类别

输入迭代器(Input iterators):可以读取序列中的元素。输入迭代器必须提供:

  • 相等和不等操作符(==,!=)用于比较两个迭代器;
  • 前置和后置递增操作符(++)用于推进迭代器;
  • 用于读取元素的解引用运算符(*);解引用只能出现在赋值运算符的右边;
  • 箭头运算符(->)等价于 (*it).member ,意味着解引用迭代器,并从底层对象中获取一个成员;

输入迭代器只能用于顺序访问。*it++ 保证是有效的,但是递增输入迭代器可能会导致流中的所有其它迭代器失效。结果就是,不能保证输入迭代器的状态可以保存,并且用于测试一个元素。输入迭代器因而只能用于单遍扫面算法,就是对元素值只访问一次,而不能多次访问。如果需要多次访问就要使用前向迭代器。find 和 accumulate 算法只要求输入迭代器;istream_iterator 是输入迭代器;

输出迭代器(Output iterators):可以被认为是输入迭代器的补集,只能写不能读元素。输出迭代器必须提供:

  • 用于推进迭代器的前置和后置递增运算符(++);
  • 解引用运算符(*),只能出现在赋值操作符的左侧(给输出迭代器赋值是将内容写入到底层元素);

我们只能给输出迭代器写入给定值一次。与输入迭代器一样,输出迭代器只能被用于单遍扫描算法。用于目的迭代器基本都是输出迭代器。如 copy 的第三个参数是输出迭代器。ostream_iterator 是输出迭代器。

前向迭代器(Forward iterators):可以读写给定序列。它们只能在序列的一个方向上移动。前向迭代器支持输入和输出迭代器的所有操作。而且,可以多次读写同一个元素。因此,我们可以保存前向迭代器的状态。使用前向迭代器的算法可以多次扫描序列。replace 算法需要一个前向迭代器;forward_list 上的迭代器就是前向迭代器;

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

随机访问迭代器(Random-access iterators):提供固定时间内访问序列中任何位置的能力。这些迭代器双向迭代器的所有操作。除此之外,随机访问迭代器还支持:

  • 关系操作符(< <= >= >)用于比较两个迭代器的相对位置;
  • 加法和减法操作符(+ += - -=)用于迭代器和整数。结果是推进或回退给定整数个元素的位置;
  • 减法操作符运用于两个迭代器,将返回两者之间的距离;
  • 下标操作符(iter[n])与 *(iter+n) 同义;

sort 算法要求随机访问迭代器。array ,deque ,string 和 vector 中的迭代器是随机访问迭代器,用于访问内置数组元素的指针也是。

10.5.2 按照算法的参数模式分配

算法的参数有自己的规范,理解这些规范对于理解算法本身很有帮助。绝大部分算法的参数形式是以下四种之一:

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

其中 algorithm 就是算法的名字,beg 和 end 表示输入范围。尽管几乎所有的算法都有一个输入范围,其它参数的出现则依赖于算法所做的工作。dest ,beg2 和 end2 都是迭代器,它们在算法中的角色是类似的。除了这些迭代器参数,有些算法还有额外的非迭代器参数。

有一个单一目的迭代器的算法

dest 参数表示一个目的地,这样算法可以将输出写入到这个 dest 所表示的元素中。算法总是假设能够写入足量的元素,保证目的迭代器能够容纳算法的输出是程序员自己的责任。如果 dest 直接指向容器中的元素,那么算法将其输出写入到容器中已经存在的元素中去。更为一般的是,dest 是一个插入迭代器或者 ostream_iterator。插入迭代器往容器中添加元素,因此可以保证容器中有足够的容量。ostream_iterator 将内容写入到输出流中,同样不用管需要写入多少个元素。

有第二个输入序列的算法

有单一的 beg2 或者 beg2 和 end2 参数的算法将这些迭代器当作第二个输入范围。这些算法将第二个范围内的元素与第一个范围内的元素联合起来进行运算。

如果一个算法的参数是 beg2 和 end2,那么这些参数有两个完全确定的范围:第一个输入范围 [beg, end) 和第二个输入范围 [beg2, end2)

如果算法只有一个 beg2 ,那么将 beg2 当作第二个输入范围的首元素。这个范围的尾部没有指定,相反,算法假定第二个范围从 beg2 开始,并且至少跟第一个输入范围一样大,保证这个条件成立是程序员自己的责任。

10.5.3 算法名字的约定

除了上面介绍的约定外,算法的名字和重载也有自己的约定。这些约定用于处理怎样提供操作以替代默认 <== 操作符,以及算法是将输出写入到其输入序列还是一个独立的目的地。

有些算法使用重载来传递谓词

使用谓词来替换 <== 操作符的算法,以及没有这个谓词参数的算法是重载的。其中一个版本使用元素的操作进行比较;另一个版本则有一个额外的参数表示谓词,如:

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

两个调用都会移除相邻的重复元素。第一个使用元素的 == 操作符以检查重复元素;第二个使用 comp 决定两个元素是否相等。由于两个函数的参数数目不一样,调用哪个函数不存在模糊性。

_if 的算法版本

有一个元素值的算法通常会有第二个名字(而不是重载),第二个版本的参数是一个谓词而不是元素值。以谓词为参数的算法的后缀是 _if

find(beg, end, val); // 查找输入范围内 val 的首次出现的位置
find_if(beg, end, pred); // 查找输入范围内 pred 首次为真的位置

这两个算法都是查找一个特定元素在输入序列中出现的位置。find 查找特定的值;find_if 查找使得 pred 为真的元素值;

这些算法提供要给额外的名字,而不是进行重载的原因是两个版本都具有相同数目的参数。重载可能会导致模糊性。

_copy 的算法版本

默认情况下,会调整元素的算法将调整后的元素写回到给定的输入范围中。这些算法提供了第二个版本将元素写入到给定的输出目的地,这样的版本以 _copy 为后缀:

reverse(beg, end); // 反转元素并将其写入到输入范围中
reverse_copy(beg, end, dest); // 将反转后的元素写入到 dest 中

一些算法同时提供了 _copy_if 版本。这些版本有一个目的迭代器和谓词:

remove_if(v1.begin(), v1.end(),
    [](int i) { return i%2; });
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2),
    [](int i) { return i%2; });

两个调用都使用 lambda 来判断一个元素是否为偶数。第一个版本,直接在输入序列中移除。第二个版本,将符合条件的值拷贝到 v2 中。

10.6 特定于容器的算法

与别的容器不同的是,list 和 forward_list 将几个算法定义为自己的成员。如:链表类型定义自己的 sort,merge,remove,reverse 和 unique 版本。通用的 sort 算法需要随机访问迭代器。因而,sort 不能被用于 list 和 forward_list,原因是它们分别只提供双向迭代器和前向迭代器。

其它一些通用算法可用于链表类型,但是会损失性能。这些算法交换输入序列中的元素。链表可以通过交换链接来达到目的,而不是交换元素的值。因而,链表特定的版本会比通用版本有一个更好的性能。

以下是链表特定的算法,没有出现在下面的算法在链表和别的容器类型上表现一样好:

  • lst.merge(lst2) lst.merge(lst2, comp) 将 lst2 上的元素合并到 lst 上。lst 和 lst2 都必须是排序了的。元素将从 lst2 中移除,在 merge 之后 lst2 将是空的。第一个版本使用 < 操作符;第二个版本使用给定的比较操作;
  • lst.remove(val) lst.remove_if(pred) 使用 erase 移除每一个 == 给定值或者使得谓词为真的元素;
  • lst.reverse() 反转 lst 中的元素顺序;
  • lst.sort lst.sort(comp) 对 lst 中的元素进行排序,第一个函数使用 < 操作符,第二个使用给定的比较操作;
  • lst.unique() lst.unique(pred) 调用 erase 移除相邻相等的元素值,第一个版本使用 ==;第二个版本使用谓词;

以上操作返回 void ,对于 list 和 forward_list 有限使用成员版本而不是通用算法版本。

splice 成员

链表类型还定义了 splice(拼接) 算法,这是链表特有的算法 list.splice(args) 以及 forward_list.splice_after(args)

  • (p, list2) 将 lst2 中的元素移动到 p 迭代器所表示为位置前,并从 lst2 中移除元素。如果是 splice_after 则拼接到 p 之后,lst2 的类型必须与 list 和 forward_list 的类型相同,而且不能与调用者是同一个对象;
  • (p, lst2, p2) p2 是 lst2 中的有效迭代器,将 p2 所表示的元素移动到 list 中,或者将 p2 后面的那个对象移动到 forward_list 中,lst2 可以与 list 或者 forward_list 是相同的对象;
  • (p, lst2, b, e) 将 lst2 中的迭代器 b 和 e 所表示的范围中的元素移动到 list 或 forward_list 中,元素会从 lst2 中删除。lst2 与 list 或者 forward_list 可以是相同的对象,但 p 不能表示 b 和 e 范围内的元素;

链表特有的算法类似于其通用算法版本。然而一个重大的区别就是链表版本会改变其底层容器。如链表版本的 remove 会真的执行移除操作,链表版本的 unique 会移除第二个及之后的重复元素。

同样的 merge 和 splice 会改变其参数,而通用版本的 merge 将内容写入到给定的目的迭代器中;两个输入序列表示不变。链表 merge 函数会销毁给定链表,将其元素从参数中移除合并到调用者链表上。在 merge 之后所有的元素都在同一个链表上。