第 10 章 泛型算法
标签: C++Primer 学习记录 泛型算法
10.1 概述
- 泛型算法,不仅作用于标准库容器,还可以适用于内置的数组类型。
- 迭代器令算法不依赖于容器,但算法本身可能依赖于元素类型的操作。如 find算法需要使用元素类型的
==
运算符、sort算法需要使用<
运算符。泛型算法本身不会执行容器的操作,它们只会运行与迭代器之上,执行迭代器的操作。即,一个算法永远不会直接改变底层容器的大小。 - 泛型算法的设计目标就是能够尽可能适应多种类型的容器,而改变容器大小的操作往往与容器类型有关。算法为了保持自己的独立性,不能使用这些依赖于容器类型的底层操作,而应该使用统一接口——迭代器。
10.2 初识泛型算法
- 只读算法。
accumulate(vec.cbegin(), vec.cend(), 0);
这里面有三个编程假定:- 序列中元素的类型必须与第三个参数匹配,或能转换为第三个参数的类型。
- 函数中使用的加法运算符也由第三个参数的类型决定。
string sum = accumulate(vec.cbegin(), vec.cend(), "")
就是错误的,因为 const char*并没有+
运算符。 - 返回值的类型与第三个参数的类型匹配。
- 对于只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
-
写入算法。**向目的位置迭代器写入数据的算法都假定目的位置足够大,能容纳要写入的元素,算法本身不会检查写操作。**可以使用插入迭代器来向容器中添加元素,back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
vector<int> vec; // 空向量 fill_n(vec.begin(), 10, 0); // 错误,向空向量写入元素! fill_n(back_inserter(vec), 10, 0); // 添加 10个元素到 vec
-
重排算法。unique,只对相邻的重复元素有效,所以使用之前需要先排序,并且其结果会使得重复元素出现在尾部!
10.3 定制操作
-
lambda表达式,可以理解为是一个未命名的内联函数。与普通函数不同,lambda必须使用尾置返回类型。形式如
[捕获列表](参数列表) ->返回类型 {函数体}
。 -
它可以忽略参数列表和返回类型,但必须永远包括捕获列表和函数体,如
auto f = [] { return 42; };
。如果忽略返回类型, lambda根据函数体中的代码推断出返回类型。- 函数体中只有一个 return语句,返回类型由返回值推断而来。
- 函数体中包含处 return之外的语句,返回 void。
-
lambda不能有默认实参,因此,一个 lambda调用的实参数目永远与形参数目相等。
-
捕获列表只用于局部非 static变量,lambda可以直接使用局部 static变量和在它所在函数之外声明的名字。在下面代码段中, cout不是自定义的局部变量,而是定义在头文件 iostream中的,但 lambda表达式中仍然可以使用该变量,只要改代码段出现的作用域中包括了头文件 iostream就可以了。
for_each(wc, words.end(), [](const string &s){cout << s << " ";});
-
当定义一个 lambda时,编译器会隐式地生成一个与 lambda对应的新的未命名的类类型。其中,捕获列表中的参数就是构造函数的参数,且是这个未命名类的数据成员 ,并且在 lambda对象创建时被初始化。而 lambda表达式中的参数与函数调用运算符的参数对应。
- 值捕获。与参数不同,被捕获的变量的值是在 lambda创建时被拷贝,而不是调用时拷贝。
size_t v1 = 42; auto f = [v1] {return v1;}; v1 = 0; auto j = f(); // J为 42;v1在 lambda创建时被 // 拷贝,随后对外面的 v1的修改与 lambda中的 v1无关
- 引用捕获。当我们在 lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。不过,当以引用方式捕获一个变量时,必须保证在 lambda执行时变量是存在的。
size_t v1 = 42; auto f = [&v1] {return v1;}; v1 = 0; auto j = f(); // J为 0;lambda中的 v1只是外面 v1的引用
-
隐式捕获。编译器会根据 lambda体中的代码来推断我们要使用哪些变量,&表示引用捕获,=表示值捕获。当混合使用了隐式和显式捕获时,捕获列表中的第一个元素必须是一个 &或=,来制定默认引用方式。另外,显式捕获的变量必须使用与隐式捕获不同的方式。
- [&, identifier_list],默认使用引用捕获方式,identifier_list中的变量都必须使用值捕获,而想要使用引用捕获的变量只能使用隐式捕获。
- [=, identifier_list],默认使用值捕获方式,identifier_list中的变量都必须使用引用捕获,而想要使用值捕获的变量只能使用隐式捕获。
-
可变 lambda。
- 对于值捕获变量,默认情况下是不可以在 lambda表达式中改变其值的。如果希望改变一个值捕获的变量的值,在参数列表后加上关键字 mutable。
size_t v1 = 42; // f能够改变它所捕获的局部变量 auto f = [v1] () mutable {return v1;}; v1 = 0; auto j = f(); // J为 43
- 而引用捕获的变量,如果此引用指向的是 const变量,则是否添加 mutable都不能在 lambda表达式中修改其值;而如果指向的是非 const变量,则不需要 mutable,默认情况下就可以在 lambda表达式中修改其值。
-
某些标准库算法只能接受一元谓词,而我们可能需要向其传递两个或多个参数,之前使用捕获列表的 lambda表达式可以完成这一任务。这里,还可以使用
bind
函数,它可以看作是一个函数适配器。- 它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
_n
是占位符,表示新调用对象的参数。这些名字都定义在名为 placeholders的命名空间中,使用之前需要声明。
using std::placeholders::_1; // check6是一个可调用对象,接受一个 string类型的参数 // 并用此 string和值6来调用 check_size auto check6 = bind(check_size, _1, 6);
- 它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
- 可以用 bind来绑定给定的可调用对象的参数或重新安排其顺序。
auto g = bind(f, a, b, _2, c, _1);
// g(x, y)会调用 f(a, b, y, c, x)
// 按单词长度由短至长排序
sort(w.begin(), w.end(), isShorter);
// 按单词长度由长至短排序
sort(w.begin(), w.end(), bind(isShorter, _2, _1));
- bind的那些不是占位符的参数都是被拷贝到 bind返回的可调用对象中的,因此,对于有些我们希望以引用方式传递或无法拷贝的类型的参数,需要使用 ref/cref。其中,ref返回一个保存给定对象的普通引用的类对象,而返回一个保存给定对象的 const引用的类对象。
void f(int& n1, int& n2, const int& n3) {
++n1;
++n2;
}
int n1 = 1, n2 = 2, n3 = 3;
auto bf_1 = bind(f, n1, ref(n2), cref(n3));
n1 = 10;
n2 = 11;
n3 = 12;
bf_1(); // 此时,n1 == 1, n2 == 12, n3 == 12。这是因为 n1是值拷贝,
// 函数之外的 n1并没变化。而 n2是引用,值会发生变化。
// 下面的语句是错误的,cref中保存的是 const引用,f中相应的参数是普通引用!
auto bf_2 = bind(f, n1, cref(n2), cref(n3));
10.4 再探迭代器
-
插入迭代器。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
- back_inserter,使用前提是底层容器支持 push_back操作,在容器末尾添加元素,不会改变插入元素的顺序。
- front_inserter,使用前提是底层容器支持 push_front操作,在容器头部添加元素,会颠倒插入元素的顺序。
- inserter(container, it),在 it所指向的元素位置之前插入新的元素,返回的迭代器仍旧指向插入之前 it所指向的元素,不会改变插入元素的顺序。
-
iostream迭代器。通过使用流迭代器,可以用泛型算法从流对象读取数据以及向其写入数据。流迭代器在绑定了输入/输出流后,就可以当做是普通的数据容器的迭代器来使用。
- istream_iterator,使用
>>
来读取流,因此 istream_iterator要读取的类型必须定义了输入运算符。此外,默认初始化迭代器,可以作为尾后值的迭代器,来表明流数据的结束。
istream_iterator<int> in_iter(cin), eof; // 从 cin读取 int vector<int> vec(in_iter, eof); // 从迭代器范围构造 vec
- ostream_iterator,要输出的类型必须定义了
<<
输出运算符。可以提供第二个参数,表示在输出每个元素后都会打印的字符串。另外,结合 copy算法一起使用,比编写循环更为简单。不允许空的或表示尾后位置的 ostream_iterator。
ostream_iterator<int> out_iter(cout, " "); for (auto e : vec) *out_iter++ = e; // 该赋值语句实际上将元素写到 cout cout << endl;
- istream_iterator,使用
-
反向迭代器,从尾元素向首元素反向移动的迭代器。递增(++it)一个反向迭代器会移动到前一个元素,递减(--it)会移动到下一个元素。
- 能定义反向迭代器的容器需要既支持
++
,也需要支持--
,因此不能从 forward_list或一个流迭代器创建反向迭代器。 - 递增反向迭代器会在容器中反向移动,为了能在容器中正向移动,需要使用
base
成员函数将其转换回普通迭代器。
// 在一个逗号分隔的列表中查找最后一个元素并将其输出 auto rcomma = find(line.crbegin(), line.crend(), ','); // 错误,将逆序输出单词的字符 cout << string(line.crbegin(), rcomma) << endl; // 正确,从逗号的下一个位置开始读取字符直到末尾 cout << string(rcomma.base(), line.cend()) << endl;
- rcomma和 rcomma.base()指向不同的元素,这样的关系反映了左闭合区间的特性。关键点在于
[line.crbegin(), rcomma)
和[rcomma.base(), line.cend())
必须指向 line中相同的元素范围。因此,从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原始迭代器指向的并不是相同的元素。
- 能定义反向迭代器的容器需要既支持
-
移动迭代器。一般来说,普通迭代器的解引用运算符返回一个指向元素的左值,而移动迭代器的解引用运算符则生成一个右值引用。可以使用标准库的
make_move_iterator
函数将一个普通迭代器转换为一个移动迭代器。某些算法会根据迭代器解引用后得到的是左值或右值引用来调用元素类型的拷贝构造或移动构造函数。
10.5 泛型算法结构
-
按照迭代器所提供的操作从低到高来分类,分为输入、输出、前向、双向和随机访问迭代器。除输出迭代器外,一个高层类别的迭代器支持低层类别迭代器的所有操作。
-
算法除了参数规范,还遵循一套命名和重载规范。
- 一些算法使用重载形式传递一个谓词。
unique(beg, end); // 使用 == 运算符比较元素 unique(beg, end, comp); // 使用 comp 运算符比较元素
- 接受一个元素值的算法通常有一个不同名的(非重载)版本,该版本接受一个谓词代替元素值,接受谓词参数的算法都有附加的
_if
后缀。
find(beg, end, val); // 查找输入范围中 val第一次出现的位置 find(beg, end, pred); // 使用 comp 运算符比较元素
- 区分拷贝元素的版本和不拷贝的版本
reverse(beg, end); // 反转输入范围中元素的顺序 reverse_copy(beg, end, dest); // 将元素按逆序拷贝到 dest
10.6 特定容器算法
-
链表类型 list和 forward_list定义了几个成员函数形式的算法,如 sort、merge、remove、reverse和unique。
-
对于链表类型,应该优先使用成员函数版本的算法而不是通用版本。因为链表可以通过改变元素间的链接而不是真的交换它们的值来“交换”元素,因此,其性能要比通用算法好得多。其中,通用 sort算法要求随机访问迭代器,因此不能用于list和 forward_list。
-
链表特有的操作会改变底层的容器,而通用算法是不会改变底层容器的。