标准库容器定义的操作很少,它并未给每个容器添加大量功能,而是提供一组算法,这些算法是通用的(generic,或称泛型的):可用于不同容器类型和不同元素类型。
顺序容器只定义了很少的操作:增删元素、访问首尾元素、确定容器是否为空、获取首元素和尾后元素迭代器。其它操作,如:查找元素、替换或删除特定值、重排元素顺序等,标准库均未定义相应的成员函数,而是定义了一组泛型算法(generic algorithm):它们实现了一些经典算法的公共接口,而且可用于不同元素类型和多种容器类型,不仅包括标准库类型,还包括内置数组类型。
概述
大多数算法都定义在头文件 algorithm
中,标准库还在头文件 numeric
中定义了一组数值泛型算法。这些算法一般不会直接操作容器,而是遍历迭代器范围。
通常,算法遍历范围并对其中每个元素进行一些处理。比如,标准库算法 find
:
- 前两个参数是迭代器范围,第三个参数是一个值。
- 返回指向范围中第一个等于给定值的元素的迭代器,若无匹配元素,则返回第二个参数表示搜索失败。
int val = 42;
auto res = find(vec.begin(), vec.end(), val);
int ia[] = {27, 210, 12, 47, 109, 83};
int vala = 83;
int *resp = find(begin(ia), end(ia), vala);
int *resp2 = find(ia + 1, ia + 4, vala);
虽然迭代器的使用让算法不依赖于容器类型,但大多数算法都使用了一个或多个元素类型上的操作,比如:find
使用 ==
运算符进行比较,其它算法可能要求元素类型支持 <
运算符。
泛型算法本身不会执行容器操作,只执行迭代器操作。这蕴含了一个编程假定:算法永远不会主动改变底层容器的大小。算法可能改变容器中保存的元素值,也可能在容器中移动元素,但永远不会直接增删元素。
标准库定义了一类特殊的迭代器,称为插入器(inserter),当给插入器赋值时,会在底层容器上执行插入操作,但算法自身不会直接对容器执行增删操作。
初识泛型算法
标准库提供了超过 100 个算法,这些算法的结构一致。除了少数例外,标准库算法都对一个范围内的元素进行操作,此元素范围称为输入范围,且总是通过算法的前两个参数所构成的迭代器范围来指定。虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本方法就是了解它们是否读取、修改或重排元素。
只读算法
一些算法对数据只读,比如:find
、count
、accumulate
等。accumulate
定义在 numeric
头文件中:
- 前两个参数表示待求和的元素范围,第三个参数是和的初值,它的类型决定了所使用的加法运算符以及返回值类型。
- 将第三个参数作为求和起点,蕴含了一个编程假定:将元素类型加到和的类型上的操作必须是可行的,即,序列中元素类型必须与第三个参数匹配,或能转为第三个参数类型。
int sum = accumulate(ivec.cbegin(), ivec.cend(), 0);
string sum = accumulate(svec.cbegin(), svec.cend(), string(""));
对于只读算法,通常最好用 cbegin
、cend
,如果想要用算法返回的迭代器来修改元素的值,则需要使用 begin
、end
。
equal
也是只读算法:
- 前两个参数表示第一个序列中的元素范围,第三个参数是指向第二个序列首元素的迭代器。
- 它将第一个序列中的每个元素与第二个序列中的对应元素进行比较,以确定两个序列是否保存相同的值。
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
equal
可以比较两个不同类型的容器,元素类型也不必相同,只要能用 ==
比较即可。equal
基于一个非常重要的假定:第二个序列至少与第一个序列一样长。
那些只接受单个迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
写容器元素的算法
一些算法将新值赋给序列中的元素,使用这类算法时,必须注意确保序列原大小不小于算法要写入的元素数目。算法不会执行容器操作,因此它们自身不可能改变容器大小。
一些算法向输入范围写入元素,比如,fill
算法:
- 前两个参数是一个迭代器范围,第三个参数是一个值。
- 它将该值赋给输入序列中的每个元素。
fill(vec.begin(), vec.end(), 0);
一些算法从两个序列中读取元素,这两个序列的元素可以来自不同类型的容器,元素类型也不要求严格匹配,只要能够比较序列中的元素,比如:
equal
算法只要求能用==
比较元素。操作两个序列的算法之间的区别在于如何传递第二个序列:
一些算法接受三个迭代器
- 前两个表示第一个序列的范围。
- 第三个表示第二个序列的首元素。
其它算法接受四个迭代器
- 前两个表示第一个序列的范围。
- 后两个表示第二个序列的范围。
用单个迭代器表示第二个序列的算法假定了第二个序列至少与第一个序列一样长,这件事情由程序员保证。
算法不检查写操作
有些算法接受一个迭代器指定目标位置,这些算法将新值赋给从目标位置开始的序列,比如 fill_n
:
- 接受三个参数,分别为迭代器、计数值、填充值。
- 它将给定的填充值赋给迭代器开始的指定数目的元素。
fill_n(dest, n, val);
fill_n
假定写入指定数目的元素是安全的,即,dest
指向一个元素,从 dest
开始的序列至少包含 n
个元素。
向目标位置迭代器写入数据的算法假定目标位置足够大,能容纳要写入的元素。
back_inserter
介绍
插入迭代器(insert iterator)是一种保证算法有足够元素空间来容纳输出数据的方法。对插入迭代器赋值,会将元素添加到容器中。
back_inserter
是定义在头文件 iterator
中的一个函数。它以引用的方式接受一个容器,返回一个与该容器绑定的插入器。当给插入器赋值时,赋值运算符会调用 push_back
将一个元素添加到容器中。back_inserter
常用于创建迭代器,并作为算法的目标位置使用。
vector<int> vec;
fill_n(back_inserter(vec), 10, 0);
拷贝算法
copy
算法:
- 接受三个迭代器,前两个表示输入范围,第三个表示目标序列起始位置。
- 它将输入范围中的元素拷贝到目标序列中,返回目标位置迭代器(最后一个拷贝目的位置之后的位置)。目标序列至少包含和输入序列一样多的元素。
int a1[] = {0, 1, 2, 3, 4, 5};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret = copy(begin(a1), end(a1), a2);
多个算法都提供了 “拷贝” 版本,这些版本的算法计算新元素的值,然后创建一个新序列保存这些结果。比如,replace
算法:
- 接受四个参数,前两个迭代器表示输入序列,第三个是待替换值,第四个是新值。
- 它将输入序列中所有等于给定值的元素都改为另一个值。
replace(ilist.begin(), ilist.end(), 0, 42);
replace_copy
算法接受额外的第三个迭代器参数,指出调整后序列的保存位置,而不会改变输入序列。
replace_copy(ilist.begin(), ilist.end(), back_inserter(vec), 0, 42);
重排容器元素的算法
某些算法会重排容器中元素顺序,比如,sort
:
- 接受两个迭代器表示待排序的元素范围。
- 它利用元素类型的
<
运算符来对输入序列排序。
标准库算法 unique
也会重排元素,使得不重复的元素出现在开头部分,返回一个指向最后一个不重复元素之后位置的迭代器。unique
算法没有真正删除元素,只是覆盖相邻的重复元素。
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
标准库算法对迭代器而非容器操作,因此算法不能直接对容器增删元素。
定制操作
很多算法默认使用元素类型的 <
或 ==
运算符比较输入序列中的元素。标准库为这些算法定义了额外的版本,允许使用自定义操作代替默认运算符。
向算法传递函数
谓词(predicate)是一个可调用的表达式,返回一个能用作条件的值。标准库算法使用的谓词有两类:
- 一元谓词(unary predicate),只接受单一参数。
- 二元谓词(binary predicate),接受两个参数。
接受谓词参数的算法对输入序列中的元素调用谓词,元素类型必须能转换为谓词的参数类型。
sort
算法的第二个版本接受第三个用作谓词的参数,该谓词必须在输入序列中所有可能的元素值上定义一个一致的序。stable_sort
是稳定排序算法,维持相等元素的原有顺序。
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
stable_sort(words.begin(), words.end(), isShorter);
lambda 表达式
传递给算法的谓词必须严格满足参数数目的要求。find_if
算法:
- 接受一个迭代器范围,且第三个参数是一个谓词。
- 返回第一个使谓词的返回值非零的元素,若不存在,则返回尾迭代器。
一个对象或一个表达式,如果可以使用调用运算符,则称其可调用。算法可以接受任何类别的可调用对象(callable object),比如:函数、函数指针、重载了函数调用运算符的类、lambda 表达式(lambda expression)。
lambda 表达式就是一个可调用的代码单元,可以将其理解为匿名内联函数。lambda 表达式形式如下:
[capture list](parameter list) -> return type { function body }
与普通函数不同的是,lambda 表达式可以定义在函数内部,而且只能使用尾置返回来指定返回类型。capture list(捕获列表)是 lambda 所在函数中定义的局部变量列表。
参数列表和返回类型可以忽略,但必须包含捕获列表和函数体。忽略参数列表等价于指定一个空参数列表。如果忽略了返回类型,lambda 根据函数体中的代码推断出返回类型:
- 如果函数体只有一条
return
语句,则返回类型从返回的表达式类型推断出来。 - 否则,返回类型为
void
。
向 lambda 传参
lambda 不能有默认参数,因此,lambda 调用的实参类型和数目必须和形参匹配。
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size(); });
使用捕获列表
lambda 即使出现在函数中,也只能使用那些明确指出的局部变量。lambda 通过捕获列表来指出将会使用的局部变量。lambda 以 []
开始,可以在其中提供一个逗号分隔的名字列表,这些名字都定义在它所在函数中。
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{ return a.size() >= sz; });
for_each(wc, words.end(),
[](const string &s) { cout << s << " "; });
lambda 只有在捕获列表中捕获它所在函数中的局部变量,才能在函数体中使用该变量,否则会报编译错误。
捕获列表只用于局部非
static
变量,lambda 可以直接使用局部static
变量和在它所在函数之外声明的名字。
for_each
算法:
- 接受一个迭代器范围和一个可调用对象。
- 对输入序列中每个元素调用该可调用对象。
lambda 捕获和返回
定义 lambda 时,编译器生成一个相应的新的匿名的类类型。当向一个函数传递 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是编译器生成的这个类类型的匿名对象。同样的,当使用 auto
定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。
默认情况下,从 lambda 生成的类都包含了对应所捕获变量的数据成员。lambda 的数据成员在 lambda 对象创建时初始化。
值捕获
变量捕获方式可以是值或引用。采用值捕获的前提是变量可以拷贝。被捕获变量的值拷贝发生在 lambda 创建时,而非调用时。
size_t v1 = 42;
auto f = [v1]() { return v1; };
v1 = 0;
auto j = f();
引用捕获
捕获列表中变量名前面加上 &
表示引用捕获。对变量使用引用捕获时,由于捕获的都是局部变量,因此必须确保被引用对象在 lambda 执行时是存在的。
size_t v1 = 42;
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2();
函数可以返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回 lambda,则 lambda 不能包含引用捕获。
尽量保持 lambda 的变量捕获简单化。
- 捕获一个普通变量,如:
int
、string
或其它非指针类型,通常可以采用简单的值捕获,此时只需关注变量捕获时是否有所需的值即可。- 如果捕获一个指针、迭代器或引用捕获,必须确保 lambda 执行时,绑定到迭代器、指针或引用的对象仍存在,而且需保证对象具有预期值。
一般,应尽量减少捕获的数据量,避免潜在的捕获导致的问题。而且,如有可能,应避免捕获指针或引用。
隐式捕获
除了显式列出捕获变量之外,还可以在捕获列表中添加 &
(引用捕获)或 =
(值捕获),让编译器根据 lambda 函数体推断出要捕获的变量。
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{ return s.size() >= sz; });
隐式捕获和显式捕获可以混合使用,此时捕获列表中第一个元素必须是 &
或 =
,用于指定默认捕获方式:
// os 隐式引用捕获,c 显式值捕获
for_each(words.begin(), words.end(),
[&, c](const string &s) { os << s << c; });
// os 显式引用捕获,c 隐式值捕获
for_each(words.begin(), words.end(),
[=, &os](const string &s) { os << s << c; });
混合使用显式捕获和隐式捕获时,显式捕获的变量必须和隐式捕获采用不同的方式。
可变 lambda
如果想要改变一个被捕获变量的值,必须为参数列表加上 mutable
。引用捕获的变量是否允许修改,取决于引用的是 const
还是非 const
。
size_t v1 = 42;
auto f1 = [v1]() mutable { return ++v1; };
v1 = 0;
f1(); // 43
f1(); // 44
size_t v2 = 42;
auto f2 = [&v2] { return ++v2; };
v2 = 0;
f2(); // 1
指定 lambda 返回类型
transform
算法:
- 接受三个迭代器和一个可调用对象,前两个迭代器表示输入序列,第三个迭代器表示目标位置。
- 对输入序列中每个元素调用可调用对象,并将结果写到目标位置。
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{ if (i < 0) return -i; else return i; });
参数绑定
lambda 表达式对于那些只在一两个地方使用的简单操作来说是最有用的。如果需要在很多地方使用相同操作,或者一个操作需要很多语句才能完成,通常应定义一个函数。
如果 lambda 的捕获列表为空,通常可以用函数来代替。而对于捕获局部变量的 lambda,需要使用定义在头文件 functional
中的标准库函数 bind
来实现。bind
函数可看作一个通用的函数适配器,接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。bind
调用形式如下:
auto newCallable = bind(callable, arg_list);
arg_list
是逗号分隔的参数列表,对应 callable
的参数,newCallable
是一个新的可调用对象。调用 newCallable
时,会将 arg_list
传递给 callable
并调用。arg_list
中的参数可能包含形如 _n
的占位符,对应传递给 newCallable
的第 n
个参数。
bool check_size(const string &s, string::size_type sz)
{
return s.size() >= sz;
}
size_t sz = 6;
auto wc = find_if(words.begin(), words.end(),
bind(check_size, _1, sz));
用 bind
可以绑定给定可调用对象中的参数或重排顺序。
auto g = bind(f, a, b, _2, c, _1);
g(X, Y); // 等价于 f(a, b, Y, c, X);
// 按单词长度由短至长排序
sort(words.begin(), words.end(), isShorter);
// 按单词长度由长至短排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1));
placeholders
名字 _n
都定义在头文件 functional
中一个名为 placeholders
的命名空间中,该命名空间本身定义在 std
中。为了使用这些名字,必须进行必要的声明。
using std::placeholders::_1;
using namespace std::placeholders;
形式如下的 using
语句让所有来自命名空间 namespace_name
的名字都可以直接使用。
using namespace namespace_name;
绑定引用参数
默认情况下,bind
的那些非占位符参数被拷贝到 bind
返回的可调用对象中。使用头文件 functional
中标准库函数 ref
、cref
可以引用的方式传递绑定的参数。函数 ref
返回一个可拷贝对象,它包含给定的引用。函数 cref
返回值保存的是 const
引用。
ostream &print(ostream &os, const string &s, char c)
{
return os << s << c;
}
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
旧版本 C++ 提供的绑定函数参数的语言特性限制更多,也更复杂。标准库函数
bind1st
、bind2nd
分别只绑定第一个或第二个参数,它们在新标准中已经废弃。
再探迭代器
标准库在头文件 iterator
中还定义了额外几种迭代器。
- 插入迭代器(insert iterator):绑定到容器上,可用于向容器插入元素。
- 流迭代器(stream iterator):绑定到输入或输出流上,用于遍历所有关联的 IO 流。
- 反向迭代器(reverse iterator):向后移动,除了
forward_list
之外的标准库容器都支持。 - 移动迭代器(move iterator):用于移动元素。
插入迭代器
插入器是一种迭代器适配器,接受一个容器,返回一个迭代器,若对其赋值,将调用容器操作向指定位置插入元素。
原文有笔误:
c.insert(t, p)
应为c.insert(p, t)
。
根据元素插入的位置,插入器分三种类型:
back_inserter
,创建一个使用push_back
的迭代器。front_inserter
,创建一个使用push_front
的迭代器。inserter
,创建一个使用insert
的迭代器。该函数接受的第二个参数是一个指向给定容器的迭代器,插入位置是给定迭代器所表示的元素之前。
只有容器支持
push_front
/push_back
时,才能使用front_inserter
/back_inserter
。
*it = val;
// 等价于
it = c.insert(it, val);
++it;
iostream
迭代器
标准库定义了可用于 IO 类型对象的迭代器。
istream_iterator
读取输入流。ostream_iterator
向输出流写数据。
这些迭代器将它们对应的流当作一个特定的元素序列来处理。
istream_iterator
创建流迭代器时,必须指定要读写的对象类型。istream_iterator
使用 >>
来读取流,因此要读取的类型必须定义了输入运算符。
istream_iterator<int> in_iter(cin); // 从 cin 读取 int
istream_iterator<int> eof; // 尾后迭代器
vector<int> vec(in_iter, eof);
ifstream in("afile");
istream_iterator<string> str_it(in); // 从 “afile” 读取字符串
对于一个关联到流的迭代器,一旦关联的流遇到文件尾或遇到 IO 错误,迭代器就和尾迭代器相等。
由于算法使用迭代器来处理数据,因此某些算法可以用来操作流迭代器。
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
istream_iterator
绑定到一个流时,标准库并不保证立即读取数据,具体实现可以推迟读取。标准库所保证的是,第一次解引用迭代器时,从流中读取数据已经完成。如果创建了 istream_iterator
之后,没有使用就销毁,或者正在从两个不同的对象同步读取同一个流,那么读取时机就很重要。
ostream_iterator
任何具有输出运算符 <<
的类型都可以定义 ostream_iterator
。创建 ostream_iterator
时,可以提供第二个 C 风格字符串参数,每次输出都会打印此字符串。ostream_iterator
必须绑定到一个流,不允许空的或表示尾后位置的 ostream_iterator
。
虽然 *
、++
不对 ostream_iterator
做任何操作,但最好还是使用 *
、++
,方便替换为其它迭代器。
ostream_iterator<int> out_it(cout, " ");
for (auto s: vec) {
*out_it++ = s;
}
cout << endl;
每次向 ostream_iterator
赋值时,写操作就会被提交。
反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。除了 forward_list
之外,其它容器都支持反向迭代器。通过 rbegin
、rend
、crbegin
、crend
成员可以获得指向容器尾元素和首前位置的反向迭代器。
// 从大到小排序
sort(vec.rbegin(), vec.rend());
只能从既支持 ++
也支持 --
的迭代器来定义反向迭代器。流迭代器不支持 --
,因此不能创建反向迭代器。
reverse_iterator
的成员函数 base
返回相应的普通迭代器,该迭代器与原反向迭代器指向相邻位置,从而保证了正向处理和反向处理的元素范围相同。
泛型算法结构
任何算法的最基本特性是要求迭代器提供哪些操作。算法所要求的迭代器操作可以分为 5 个迭代器类别(iterator category)。每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。
5 类迭代器
迭代器按照所提供操作的分类形成了一种层次。除了输出迭代器之外,高层迭代器支持低层迭代器的所有操作。C++ 标准指明了泛型和数值算法的每个迭代器参数的最小类别。
向一个算法传递错误类别的迭代器时,很多编译器不会给出任何警告或提示。
-
输入迭代器(input iterator)
可读取序列中的元素。输入迭代器必须支持:
- 相等和不相等运算符
==
、!=
- 前置和后置的递增运算
++
- 最多只出现在赋值运算符右侧的解引用运算符
*
- 箭头运算符
->
输入迭代器只用于顺序访问的单遍扫描算法。
istream_iterator
是一种输入迭代器,对于输入迭代器,*it++
保证有效,但是递增它可能导致所有其它指向流的迭代器失效。因此不能保证输入迭代器的状态可以保存并访问元素。 - 相等和不相等运算符
-
输出迭代器(output iterator)
只写不读。输出迭代器必须支持:
- 前置和后置的递增运算
++
- 只出现在赋值运算符左侧的解引用运算符
*
输出迭代器只能用于单遍扫描算法,只能向一个输出迭代器赋值一次。用作目标位置的迭代器通常都是输出迭代器。
ostream_iterator
是一种输出迭代器。 - 前置和后置的递增运算
-
前向迭代器(forward iterator)
可读写元素,只能单向移动,支持所有输入和输出迭代器的操作。同一元素可多次读写,因此可以保存前向迭代器的状态,并对序列多遍扫描。
forward_list
的迭代器是前向迭代器。 -
双向迭代器(bidirectional iterator)
可双向读写序列中的元素。除了前向迭代器支持的操作之外,还支持前置和后置的递减运算符
--
。除了forward_list
之外的标准库容器提供的都是双向迭代器。 -
随机访问迭代器(random-access iterator)
可在常量时间内访问序列中任意元素。除了双向迭代器支持的操作之外,还支持
- 用于比较迭代器位置的关系运算符
<
、<=
、>
、>=
- 与整数相加减
+
、+=
、-
、-=
,得到移动后位置的迭代器 - 两迭代器相减
-
,计算迭代器距离 - 下标运算符
iter[n]
,等价于*(iter + n)
array
、deque
、string
、vector
的迭代器以及内置数组元素的指针都是随机访问迭代器。 - 用于比较迭代器位置的关系运算符
算法形参模式
算法有一组参数规范。大多数算法具有如下 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);
arg
是算法名称,beg
、end
是算法操作的输入范围。迭代器参数 dest
、beg2
、end2
分别指定目标位置、第二个范围。
接受单个目标迭代器的算法
迭代器 dest
指定了写入的目标位置,此类算法假定:按其需要写入数据,不管写入多少个元素都是安全的。dest
通常是插入迭代器或 ostream_iterator
。
向输出迭代器写数据的算法都假定目标空间足够容纳写入的数据。
接受第二个输入序列的算法
迭代器 beg2
、end2
表示第二个输入范围。
- 有些算法同时接受迭代器
beg2
和end2
,表示接受第二个范围[beg2, end2)
。 - 有些算法只接受
beg2
迭代器,将beg2
作为第二个输入范围的首元素,此范围的结束位置待定。这些算法假定从beg2
开始的范围与[beg, end)
至少一样大。
算法命名规范
算法遵循一套命名和重载规范。它们规定了:如何提供一个操作代替默认的 <
、==
运算符,算法是将输出数据写入输入序列还是一个单独的目标位置。
接收谓词参数来代替 <
或 ==
运算符的算法,以及那些不接收额外参数的算法,通常都是重载函数。函数的一个版本用元素类型的运算符比较元素,另一个版本接收一个额外谓词参数代替 <
、==
:
unique(beg, end);
unique(beg, end, comp);
接受一个元素值的算法通常有另一个接受一个谓词代替元素值的 _if
版本。
find(beg, end, val);
find_if(beg, end, pred);
两个版本的算法都接受相同数目的参数,因此可能产生重载歧义,虽然很罕见,但是为避免歧义,标准库选择提供不同名字的版本而非重载。
有些写入输入序列的算法还提供另一个写入指定目标位置的 _copy
版本。
reverse(beg, end);
reverse_copy(beg, end, 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; });
特定容器算法
链表类型 list
和 forward_list
定义了几个成员函数形式的算法。它们定义了独有的 sort
、merge
、remove
、reverse
、unique
。通用版本的 sort
要求随机访问迭代器,不能用于 list
和 forward_list
。即使其它算法可用于链表,交换元素的代价也太高。链表可通过改变元素间链接来快速交换,因此链表版本算法性能比对应的通用版本好得多。
对于
list
和forward_list
,应优先使用成员函数版本的算法而不是通用算法。
链表数据结构还定义了特有的 splice
算法。
与通用版本不同的是,链表特有版本的算法会改变底层容器。