【C++ primer】第10章 泛型算法 (1)


Part II: The C++ Library
Chapter 10. Generic Algorithms


标准库定义了一系列泛型算法 (generic algorithms):

  • “算法”:实现公共典型算法,比如排序和搜索。
  • “泛型”:作用于不同类型的元素和多种容器类型,不仅是如 vector 之类的库类型,还可以用于内置数组类型,以及其他类型的序列。

大多数算法定义在 algorithm 头文件中。库还在 numeric 头文件中定义了一系列泛型数值算法。

一般情况下,这些算法不直接操作容器,而是通过遍历由两个迭代器指定的范围内的元素来操作。

迭代器令算法不依赖于容器,但算法依赖于元素类型操作。

关键概念:算法从不执行容器操作
泛型算法本身不会执行容器操作,它们仅在迭代器和迭代器操作方面进行操作。
算法不会改变底层容器的大小。算法可能改变容器存储的元素值,在容器内移动元素,但它们不会直接添加或删除元素。


10.1 初识泛型算法

库提供了 100 多个算法。附录A 按其工作方式分类,列出了所有算法。

除了几个例外,算法对一个范围内的元素进行操作。这个范围称为“输入范围”。接受输入范围的算法始终使用其前两个形参来表示该范围。这些形参是表示要处理的第一个和尾后迭代器。

算法使用范围内的元素的方式不同。理解算法的最基本方法是知道它们是读取元素,写入元素还是重新排列元素的顺序。

只读算法

// sum the elements in vec starting the summation with the value 0
int sum = accumulate(vec.cbegin(), vec.cend(), 0);

accumulate 是只读算法,定义在 numeric 头文件中。accumulate 函数接受 3 个实参,前两个指定需要求和的元素范围,第三个是和的初始值。
accumulate 第三个实参决定了使用的加法运算符和 accumulate 返回的类型。

算法和元素类型

accumulate 使用第三个实参作为求和起点,这意味着:将元素类型加到和的类型必须是可行的。即,序列中的元素必须匹配或可以转换成第三个参数的类型。

// explicitly create a string as the third parameter
string sum = accumulate(v.cbegin(), v.cend(), string(""));

// error: no + on const char*
string sum = accumulate(v.cbegin(), v.cend(), "");

对于只读算法,通常最好使用 cbegin() 和 cend()。然而,如果打算使用算法返回的迭代器来改变元素值,那么需要传递 begin() 和 end()。

在两个序列上操作的算法

equal 是只读算法,用于确定两个序列是否存储相同的值。若所有对应元素都相等,则返回 true。该算法接受 3 个迭代器:前两个表示第一个序列的元素范围,第三个表示第二个序列的首元素。

// roster2 should have at least as many elements as roster1
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

可以调用 equal 来比较不同类型容器的元素,而且,元素类型不一定要一样,只要可以使用 == 来比较元素类型就行。比如 roster1 可以是 vector<string>,roster2 可以是 list<const char*>

注意:接受单个迭代器表示第二个序列的算法假定第二个序列至少与第一个序列一样大。

写入容器元素的算法

一些算法将新值赋给序列中的元素。记住,算法不执行容器操作,因此它们本身无法更改容器的大小。
一些算法向输入范围本身写入元素。

fill 算法接受一对迭代器表示范围,第三个实参是一个值。fill 将给定的值赋给输入序列的每个元素。

fill(vec.begin(), vec.end(), 0);  // reset each element to 0
// set a subsequence of the container to 10
fill(vec.begin(), vec.begin() + vec.size()/2, 10);

算法不检查写操作

一些算法接受一个表示单独目标的迭代器。这些算法从目标迭代器表示的元素开始,为序列的元素分配新值。
例如,fill_n 函数接受一个迭代器,一个计数和一个值。它将给定值分配给指定数量的元素,该数量从迭代器指示的元素开始。

vector<int> vec;  // empty vector
// use vec giving it various values
fill_n(vec.begin(), vec.size(), 0); // reset all the elements of vec to 0 

// disaster: attempts to write to ten (nonexistent) elements in vec
fill_n(vec.begin(), 10, 0);

介绍 back_inserter

确保算法具有足够多的元素来保存输出数据的一种方法是使用插入迭代器 (insert iterator)。插入迭代器是将元素添加到容器的迭代器。
通常,当通过迭代器给容器元素赋值时,会赋值给迭代器表示的元素。
当通过插入迭代器进行赋值时,会将等于右侧值的新元素添加到容器中。

back_inserter 函数定义在 iterator 头文件中。
back_inserter 接受一个指向容器的引用,返回一个与那个容器绑定的插入迭代器。当通过此迭代器赋值时,赋值调用 push_back 将具有给定值的元素添加到容器中。

vector<int> vec; // empty vector
auto it = back_inserter(vec); // assigning through it adds elements to vec
*it = 42;        // vec now has one element with value 42

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

vector<int> vec; // empty vector
// ok: back_inserter creates an insert iterator that adds elements to vec
fill_n(back_inserter(vec), 10, 0);  // appends ten elements to vec

拷贝算法

copy 算法:写入输出序列的元素,输出序列由目标迭代器表示。
该算法接受三个迭代器:前两个表示输入范围;第三个表示目标序列的开始。
此算法将元素从其输入范围复制到目标中的元素。
注意,传递给复制的目的位置必须至少与输入范围一样大。

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)];  // a2 has the same size as a1
// ret points just past the last element copied into a2
auto ret = copy(begin(a1), end(a1), a2);  // copy a1 into a2

几种算法提供了所谓的“复制”版本。这些算法会计算新的元素值,但不会将其放回输入序列中,而是创建一个包含结果的新序列。

replace 算法读取一个序列,并将给定值的每个实例替换为另一个值。该算法接受四个参数:两个表示输入范围的迭代器和两个值。它将与第一个值相等的每个元素替换为第二个:

// replace any element with the value 0 with 42
replace(ilst.begin(), ilst.end(), 0, 42);

上面的调用将序列中的所有 0 都替换成 42。

如果希望保持原始序列不变,则可以调用 replace_copy。该算法接受第三个迭代器参数,该参数表示要在其中写入调整后的序列的目标:

// use back_inserter to grow destination as needed
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);

重排容器元素的算法

sort 算法使用元素类型的 < 运算符将输入范围中的元素按排序顺序排列。该算法接受 2 个迭代器表示需要排序的元素范围。

消除重复

为了消除重复单词,首先对 vector 排序,这样重复的单词相邻。接着使用 unique 算法,重排 vector,使得唯一的元素出现在 vector 的第一部分。

void elimDups(vector<string> &words) {
	// sort words alphabetically so we can find the duplicates
	sort(words.begin(), words.end());
	// unique  reorders the input range so that each word appears once in the
	// front portion of the range and returns an iterator one past the unique range
	auto end_unique = unique(words.begin(), words.end());
	// erase uses a vector operation to remove the nonunique elements
	words.erase(end_unique, words.end());
}

使用 unique

unique 算法重排输入范围“消除”相邻的重复条目,返回一个迭代器,指向唯一值范围的的末尾。

unique 不会删除任何元素,而是覆盖相邻的重复元素,这样,唯一值出现在序列的前面。unique 返回的迭代器指向最后唯一元素的后一个位置。

使用容器操作删除元素

为了真正地删除无用元素,必须使用容器操作,此处调用 erase。


10.2 定制操作

许多算法会比较输入序列的元素。默认情况下,这样的算法使用元素类型的 <== 操作符。库也定义了这些算法的另一版本,允许我们提供自己的操作符替代默认操作符来使用。

向算法传递函数

如果想要按照单词的长度排序,需要使用 sort 的第二个重载版本。
这个版本的 sort 接受第三个参数,该参数是一个谓词 (predicate)。

谓词

谓词是一个可调用的表达式,返回一个可用作条件的值。

库算法使用的谓词有两种:

  • 一元谓词 (unary predicates):有一个参数
  • 二元谓词 (binary predicates):有两个参数

接受谓词的算法在输入范围内的元素上调用给定的谓词。因此,元素类型必须能够转换成谓词参数类型。

接受一个二元谓词的 sort 版本使用给定的谓词替代 < 来比较元素。

// comparison function to be used to sort by word length
bool isShorter(const string &s1, const string &s2) {
	return s1.size() < s2.size();
}
// sort on word length, shortest to longest
sort(words.begin(), words.end(), isShorter);

排序算法

stable_sort 是一个稳定排序算法。稳定排序算法维持相等元素的原有顺序。

elimDups(words); // put words in alphabetical order and remove duplicates
// resort by length, maintaining alphabetical order among words of the same length
stable_sort(words.begin(), words.end(), isShorter);

lambda 表达式

根据算法接受一元还是二元谓词,传递给算法的谓词必须分别正好接受一个或两个参数。
然而,有时想要进行需要更多参数的处理,超过了算法谓词的允许。

例,求大于等于给定长度的单词有多少,并将这些单词打印出来。

void biggies(vector<string> &words, vector<string>::size_type sz) {
	elimDups(words); // put words in alphabetical order and remove duplicates
	// resort by length, maintaining alphabetical order among words of the same length
	stable_sort(words.begin(), words.end(), isShorter);
	// get an iterator to the first element whose size() is >= sz
	// compute the number of elements with size >= sz
	// print words of the given size or longer, each one followed by a space
} 

可以使用库 find_if 算法找出具有特定大小的元素。该算法接受两个迭代器表示范围,第三个参数是一个谓词。find_if 算法在输入范围内元素上调用给定的谓词,返回第一个使谓词返回非0的元素,如果不存在这样的元素,则返回尾迭代器。

但是,find_if 接受一元谓词,可以接受一个 string,无法传递表示大小的第二个实参。

介绍 lambda

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

有四种可调用对象:函数、函数指针、重载函数调用运算符的类(第14章)、lambda 表达式 (lambda expressions)。

一个 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; };
cout << f() << endl;  // prints 42 

在 lambda 中忽略括号和形参列表等价于指定一个空的形参列表。
如果忽略返回类型,那么 lambda 的返回类型取决于函数体中的代码。如果函数体只是一个 return 语句,返回类型由返回的表达式类型推断而来;否则,返回类型是 void。

注意:如果 lambda 没有指定返回类型,且它的函数体中除了一个 return 语句外,还包含其他语句,那么它返回 void。

向 lambda 传递参数

lambda 不能有默认实参。所以,调用 lambda 时实参的个数与它的形参一样多。

写一个 lambda,与 isShorter 函数功能相同:

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

捕获列表为空,表示该 lambda 不会使用它所在函数的任何局部变量。

// sort words by size, but maintain alphabetical order for words of the same size
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size();});

使用捕获列表

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

lambda 想要使用其所在函数中的局部变量,必须在其捕获列表中捕获该变量。

// error: sz not captured
[](const string &a) { return a.size() >= sz; };

调用 find_if

// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });

// compute the number of elements with size >= sz
auto count = words.end() - wc;
// if count equal to 1, make_plural return "word"; otherwise, return "words"
cout << count << " " << make_plural(count, "word", "s") << " of length " << sz << " or longer" << endl;

for_each 算法

// print words of the given size or longer, each one followed by a space
for_each(wc, words.end(), [](const string &s){cout << s << " ";});
cout << endl;

上面 lambda 中的捕获列表是空的,但函数体使用了两个名字:它自己的参数 s,和 cout。

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

lambda 捕获和返回

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

默认情况下,从 lambda 生成的类包含数据成员,这些成员与 lambda 捕获的变量相对应。创建 lambda 对象时会初始化 lambda 的数据成员。

表10.1 lambda捕获列表

捕获列表说明
[]空捕获列表。lambda 不可使用其所在函数的变量。
[names]names 是逗号分隔的名字列表,这些名字是 lambda 所在函数的局部变量。
默认情况下,捕获列表中的变量会被复制。如果在名字前加上 &,则是引用捕获。
[&]隐式捕获,使用引用捕获列表。lambda 体内所使用的来自其所在函数中的实体通过引用方式使用。
[=]隐式捕获,使用值捕获列表。lambda 体内所使用的来自其所在函数中的实体会被复制到 lambda 体内。
[&, identifier_list]identifier_list 是逗号分隔的列表,包含 0 个或多个来自 lambda 所在函数中的变量。
捕获这些变量通过值方式;任何隐式捕获的变量是通过引用方式捕获的。identifier_list 中的名字前面不能使用 &。
[=, reference_list]reference_list 中的变量通过引用方式捕获;任何隐式捕获的变量是通过值方式捕获的。
reference_list 中的名字不能包括 this,且名字前必须加上 &。

值捕获

与参数传递类似,捕获变量可以通过值或引用的方式。

使用值捕获的前提是,变量必须可以复制。捕获的变量值的复制发生在 lambda 创建时,而不是在它调用时。

void fcn1() {
	size_t v1 = 42;  // local variable
	// copies v1 into the callable object named f
	auto f = [v1] { return v1; };
	v1 = 0;
	auto j = f(); // j is 42; f stored a copy of v1 when we created it
}

引用捕获

void fcn2() {
	size_t v1 = 42;  // local variable
	// the object f2 contains a reference to v1
	auto f2 = [&v1] { return v1; };
	v1 = 0;
	auto j = f2(); // j is 0; f2 refers to v1; it doesn't store it
}

警告:当通过引用方式捕获变量时,必须确保在 lambda 执行时变量是存在的。

引用捕获有时是必要的。下面代码中,ostream 对象不能复制,所以捕获 os 的唯一方式是使用引用(或指向 os 的指针)。

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ')
{
	// code to reorder words as before
	// statement to print count revised to print to os
	for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });
}

可以从函数中返回 lambda。函数可直接返回一个可调用对象,或返回一个含有可调用对象的数据成员的类对象。
如果函数返回一个 lambda,那么 lambda 不能包含引用捕获。

建议:保持 lambda 捕获简单化

一般来说,可以通过最小化捕获数据,以避免潜在的捕获问题。而且,如果可能的话,避免捕获引用或指针。

隐式捕获

除了显式列出想要使用的来自所在函数的变量,还可以让编译器从 lambda 中的代码推断使用的变量。
为了指示编译器推断捕获列表,在捕获列表中使用 =&
= 告诉编译器是值捕获。
& 告诉编译器是引用捕获。

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

如果想要对一部分变量使用值捕获,对其他使用引用捕获,可以混合使用隐式和显式捕获:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ')
{
	// other processing as before
	// os implicitly captured by reference; c explicitly captured by value
	for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c; });
	// os explicitly captured by reference; c implicitly captured by value
	for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c; });
}

当混合使用隐式和显式捕获时,捕获列表的第一项必须是 = 或 &。
当混合使用隐式和显式捕获时,显式捕获的变量的使用形式必须与隐式捕获的变量不同。

可变 lambda

默认情况下,lambda 不能改变通过值方式复制的变量的值。
如果希望能够改变捕获的变量的值,必须在形参列表后面加上关键字 mutable
可变 lambda 可以省略形参列表。

void fcn3() {
	size_t v1 = 42; // local variable
	// f can change the value of the variables it captures
	auto f = [v1] () mutable { return ++v1; };
	v1 = 0;
	auto j = f(); // j is 43
}

通过引用捕获的变量是否可以改变,只取决于这个引用是 const 还是非 const 类型:

void fcn4() {
	size_t v1 = 42;  // local variable
	// v1 is a reference to a non const variable
	// we can change that variable through the reference inside f2
	auto f2 = [&v1] { return ++v1; };
	v1 = 0;
	auto j = f2(); // j is 1
}

指定 lambda 返回类型

默认情况下,如果 lambda 体中除了 return 语句还包含其他语句,那么这个 lambda 返回 void。

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

在上面的代码中,使用库 transform 算法和一个 lambda,将序列中的每个负值替换成其绝对值。
如上面调用所示,目标位置迭代器可以与表示输入开始位置的迭代器一样。

对于上面的 lambda,无须指定返回类型,因为可以根据条件运算符的类型推断出来。
但是,如果将程序改写为看似相等的 if 语句,代码编译错误:

// error: cannot deduce the return type for the lambda
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; });

绑定参数

如果在许多地方需要同样的操作,通常应该定义一个函数而不是多次写相同的 lambda 表达式。
如果一个操作需要许多语句,那么使用函数更好。

如果 lambda 捕获列表为空,那么通常可以直接用函数替换它。

用在 find_if 调用中的 lambda 比较一个 string 和一个给定大小。很容易写一个函数完成同样的工作:

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

但不能使用这个函数作为 find_if 的参数。为了使用 check_size 替代那个 lambda,必须弄清楚如何向 sz 形参传递实参。

库 bind 函数

要解决向 check_size 传递一个表示大小的实参的问题,可以使用库函数 bind,它定义在 functional 头文件中。
bind 函数可以被看作是一个通用的函数适配器。它接受一个可调用对象,生成一个新的可调用对象“适应”原对象的形参列表。

调用 bind 的一般形式是:

auto newCallable = bind(callable, arg_list);

当调用 newCallable 时,newCallable 调用 callable,将 arg_list 中的实参传递给它。

arg_list 中的实参可能包含 _n 形式的名字,其中 n 是一个整数。这些实参是“占位符”(placeholders),表示 newCallable 的形参。它们占据“替代”将传递给 newCallable 的实参。数字 n 是生成的可调用对象形参的位置:_1 是 newCallable 中第一个形参,_2 是第二个,等等。

绑定 check_size 中的 sz 形参

// check6 is a callable object that takes one argument of type string
// and calls check_size on its given string and the value 6
auto check6 = bind(check_size, _1, 6);

上面的 bind 的调用只有一个占位符,这意味着 check6 只接受一个实参。该占位符出现在 arg_list 的第一个位置,表示 check6 的这个形参对应于 check_size 的第一个形参,其类型是 const string&。

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

使用 bind,可以替换原来基于 lambda 的 find_if 的调用:

auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
// a version that uses check_size
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz)); 

使用 placeholders 名字

名字 _n 定义在名为 placeholders 的命名空间中。这个命名空间本身定义在 std 命名空间中。

using namespace std::placeholders;

与 bind 函数类似,placeholders 命名空间定义在 functional 头文件中。

使用 bind 重排形参顺序

// sort on word length, shortest to longest
sort(words.begin(), words.end(), isShorter);
// sort on word length, longest to shortest
sort(words.begin(), words.end(), bind(isShorter, _2, _1));

绑定引用参数

// os is a local variable referring to an output stream
// c is a local variable of type 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 的捕获,因为 bind 复制其实参,而 ostream 不能复制。

// error: cannot copy os
for_each(words.begin(), words.end(), bind(print, os, _1, ' '));

如果想要向 bind 传递一个对象而不用复制它,必须使用库 ref 函数。

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

ref 函数返回一个包含给定引用的对象,这个对象本身是可复制的。
cref 函数生成一个类存储 const 引用。
ref 和 cref 都定义在 functional 头文件中。


【C++ primer】目录

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值