12
算法
若无必要,勿增实体。
—— 威廉·奥卡姆1
12.1 导言
如果单打独斗,vector
和list
这些数据结构的用途颇为有限。
使用时,我们需要添加和删除元素这些基本操作(就像list
和vector
实现的那样)。
不过,使用容器仅仅存储对象的情况寥寥无几。
而是还要排序、打印、提取子集、移除元素、查找对象等等。
相应地,标准库里除了最常见的容器类型之外,还为这些容器提供了最常见的算法。
例如,我们可以简单高效地把持有Entry
的vector
进行排序,
并为vector
中每个不重复的元素在list
中创建副本:
void f(vector<Entry>& vec, list<Entry>& lst)
{
sort(vec.begin(),vec.end()); // 用 < 进行排序
unique_copy(vec.begin(),vec.end(),lst.begin()); // 不复制相邻的等值元素
}
要让这段代码运行,必须为Entry
定义小于(<
)和等于(==
)操作。例如:
bool operator<(const Entry& x, const Entry& y) // 小于
{
return x.name<y.name; // 用 name 给 Entry 排序
}
标准算法以元素的(半开)序列的方式表示。 一个序列(sequence)以指向首元素和尾元素后位置的一对迭代器表示:
在本例中,sort()
为vec.begin()
和vec.end()
定义的 迭代器对 定义的序列排序,该 迭代器对 恰好包括了vector
的全部元素。
对于写(输出)操作,仅需指定待写入的首元素位置。
如果有不止一个元素输出,起始元素后的元素会被覆盖。
就是说,要避免出错,lst
的元素数量至少跟vec
中不重复值的数量一样多。
如果要把不重复元素放进一个新(空的)容器里,应该这么写:
list<Entry> f(vector<Entry>& vec)
{
list<Entry> res;
sort(vec.begin(),vec.end());
unique_copy(vec.begin(),vec.end(),back_inserter(res)); // 添加到 res 尾部
return res;
}
back_inserter(res)
这个调用为res
在容器末尾构造一个迭代器,
并且用它添加这些元素,为它们扩展容器以便提供存储空间。
这样我们就盛事了,不必去先分配一块固定容量的空间,而后再填充它。
于是,标准容器加上back_inserter()
消灭了对realloc()
——这个易出错的显式C-风格内存管理——的需求。
标准库容器list
有个转移构造函数(§5.2.2),
它可以令res
的传值返回变得高效(哪怕是装载着成千上万元素的list
)。
如果你觉得这个 迭代器对 风格的代码
——例如sort(vec.begin(),vec.end())
——繁琐,
还可以定义容器版本的算法,然后这么写sort(vec)
(§12.8)。
12.2 迭代器应用
对于某个容器,有几个指向特定元素的迭代器可以获取;
begin()
和end()
就是这种例子。
另外,很多算法也会返回迭代器。
例如,标准算法find()
在某个序列中查找一个值并返回指向该元素的迭代器:
bool has_c(const string& s, char c) // s 是否包含字符 c ?
{
auto p = find(s.begin(),s.end(),c);
if (p!=s.end())
return true;
else
return false;
}
与很多标准库查找算法相似,find()
返回end()
以表示“未找到(not found)”。
一个等价且更简短的has_c()
定义是:
bool has_c(const string& s, char c) // s 是否包含字符 c ?
{
return find(s.begin(),s.end(),c)!=s.end();
}
一个更有意思的练习是找到一个字符在某字符串中出现的所有位置。
我们可以返回以承载 string
迭代器 的vector
,以表示的出现位置集合。
返回vector
是高效的,因为它提供了转移语意(§5.2.1)。
如果我们想对找到的位置进行修改,就需要传入 非-const
字符串:
vector<string::iterator> find_all(string& s, char c) // 查找s中出现的所有c
{
vector<string::iterator> res;
for (auto p = s.begin(); p!=s.end(); ++p)
if (*p==c)
res.push_back(p);
return res;
}
我们用一个常规的循环在这个字符串中进行遍历,
借助++
每次把迭代器p
向容器尾部移动一个元素,
并借助解引用操作符*
查看这些元素。可以这样测试find_all()
:
void test() {
string m {"Mary had a little lamb"};
for (auto p : find_all(m,'a'))
if (*p!='a')
cerr << "a bug!\n";
}
上面的find_all()
调用可图示如下:
对于每个合乎情理的使用情形而言,迭代器和标准算法在所有标准容器上的应用都是等效的。
因此,可以这样泛化find_all()
:
template<typename C, typename V>
vector<typename C::iterator> find_all(C& c, V v) { // 查找v中出现的所有c
vector<typename C::iterator> res;
for (auto p = c.begin(); p!=c.end(); ++p)
if (*p==v)
res.push_back(p);
return res;
}
为了让编译器知道C
的iterator
应该被推断为类型而非一个值,比方说整数7
,
那个typename
是必须的。
可以为iterator
引入一个类型别名(§6.4.2)以隐藏这个实现细节:
template<typename T>
using Iterator = typename T::iterator; // T的迭代器
template<typename C, typename V>
vector<Iterator<C>> find_all(C& c, V v) // 查找v中出现的所有c
{
vector<Iterator<C>> res;
for (auto p = c.begin(); p!=c.end(); ++p)
if (*p==v)
res.push_back(p);
return res;
}
现在可以这样写:
void test()
{
string m {"Mary had a little lamb"};
for (auto p : find_all(m,'a')) // p是个 string::iterator
if (*p!='a')
cerr << "string bug!\n";
list<double> ld {1.1, 2.2, 3.3, 1.1};
for (auto p : find_all(ld,1.1)) // p是个 list<double>::iterator
if (*p!=1.1)
cerr << "list bug!\n";
vector<string> vs { "red", "blue", "green", "green", "orange", "green" };
for (auto p : find_all(vs,"red")) // p是个 vector<string>::iterator
if (*p!="red")
cerr << "vector bug!\n";
for (auto p : find_all(vs,"green"))
*p = "vert";
}
迭代器的作用是分离算法和容器。
算法通过迭代器操作数据,并对元素所在的容器一无所知。
反之,容器对操作元素的算法也是不知所以;
它所做的不过是按需提供迭代器(即begin()
和end()
)而已。
这种把数据存储和算法分离的模型催生出了泛化且灵活的软件。
12.3 迭代器类型
到底什么是迭代器?任何迭代器都是某种类型的一个对象。
只不过,有着很多种不同的迭代器类型,
因为一个迭代器需要为特定的容器类型保存作业所需的信息。
这些迭代器类型可以像容器那般多种多样,还可以按实际情况进行特化。
例如,vector
的迭代器可以是普通的指针,
因为需要指向vector
元素的时候,指针是个相当合理的的方式:
或者,vector
迭代器也可以实现为指向vector
的指针外加一个索引:
使用这种迭代器可以进行越界检查。
list
迭代器不得不比指向元素的指针更复杂一些,
因为一般来说list
的元素并不知道该list
中下一个元素在哪儿。
因此,list
的迭代器有可能是个指向节点的指针:
所有迭代器共通的部分是它们的语意和操作的命名。
例如,对任何迭代器应用++
都会得到指向指向下一个元素的迭代器。
类似地,用*
可以得到该迭代器指向的元素。
实际上,任何对象,只要符合几个诸如此类的简单规则就是一个迭代器
——迭代器(Iterator)是个概束(§7.2,§12.7)。
另外,用户极少需要知道具体迭代器的类型;每个容器都“知道”它自己迭代器类型,
并按惯例以iterator
和const_iterator
为名称提供它们。
例如,list<Entry>::iterator
就是list<Entry>
的通用迭代器类型。
我们几乎没必要操心该类型定义的细节。
12.4 流迭代器
在处理容器中的元素序列时,迭代器是个通用且便利的概念。 但是,容器并非元素序列栖身的唯一所在。 例如,一个输入流产生一个值序列,另外,我们会向输出流写入值序列。 所以,迭代器的概念在输入和输出方面的应用也颇为有益。
要得到一个ostream_iterator
,需要指定使用的流以及待写入对象的类型。
例如:
ostream_iterator<string> oo {cout}; // 向cout写入string
给*oo
赋值待结果就是把该值写入到cout
。例如:
int main()
{
*oo = "Hello, "; // 意思是cout<<"Hello, "
++oo;
*oo = "world!\n"; // 意思是cout<<"world!\n"
}
这是另一种将规范化消息写向标准输出的方式。
++oo
模拟了利用指针向数组写入的行为。
类似地,istream_iterator
允许我们把一个输入流作为只读容器使用。
同样,还是要指定使用的流和待读取对象的类型:
istream_iterator<string> ii {cin};
输入迭代器通常成对出现来表示一个序列,
因此还需要提供一个istream_iterator
以表示输入的末尾。
这就是默认的istream_iterator
:
istream_iterator<string> eos {};
一般来说,istream_iterator
和ostream_iterator
不会直接拿来就用。
而是会作为参数传递给算法去使用。
例如,可以写个简单的程序读取文件,把读到的单词排序、去重,
然后把结果写入到另一个文件:
int main()
{
string from, to;
cin >> from >> to; // 获取源文件名和目标文件名
ifstream is {from}; // 以"from"文件作为输入流
istream_iterator<string> ii {is}; // 流的输入迭代器
istream_iterator<string> eos {}; // 输入截止信号
ofstream os {to}; // 以"to"文件作为输出流
ostream_iterator<string> oo {os,"\n"}; // 流的输出迭代器
vector<string> b {ii,eos}; // b 是个以输入流初始化的vector
sort(b.begin(),b.end()); // 给缓存排序
unique_copy(b.begin(),b.end(),oo); // 把缓存复制到输出流,丢弃重复的值
return !is.eof() || !os; // 返回错误状态(§1.2.1, §10.4)
}
ifstream
是个可附着到文件上的istream
,
而一个ofstream
是个可以附着到文件的ostream
(§10.7)。
ostream_iterator
的第二个参数用于分隔输出值。
实际上,此程序没必要写这么长。
我们将字符串读取到vector
,然后给它们sort()
,继而去重再输出。
更优雅的方案是根本不存储重复值。
要做到这一点,可以把string
存储在set
中,
set
不会保存重复的值,并且会维持元素的顺序(§11.4)。
这样,可以把使用vector
的两行代码以使用set
的一行取代,
并使用更简单的copy()
取代unique_copy()
:
set<string> b {ii,eos}; // 从输入流收集字符串
copy(b.begin(),b.end(),oo); // 把缓存复制到输出流
ii
、eos
和oo
都只用了一次,因此可以进一步缩减程序的代码量:
int main()
{
string from, to;
cin >> from >> to; // 获取源文件名和目标文件名
ifstream is {from}; // 以"from"文件作为输入流
ofstream os {to}; // 以"to"文件作为输出流
set<string> b {istream_iterator<string>{is},istream_iterator<string>{}}; // 读输入流
copy(b.begin(),b.end(),ostream_iterator<string>{os,"\n"}); // 复制到输出流
return !is.eof() || !os; // 返回错误状态(§1.2.1, §10.4)
}
至于最后一步简化是否提高可读性,取决于个人偏好和经验。
12.5 谓词
到目前为止,例子中的算法对序列中的元素执行简单的“内建(built in)”操作。
但是,我们经常需要把这个操作作为参数传给算法。
比方说,算法find
(§12.2,§12.6)提供了便捷的方式查找特定的值。
有个更通用的变体可以查找一个符合特定条件——谓词(predicate)——的元素。
例如,我们可能需要在一个map
里查找第一个大于42
的值。
map
对其元素以(key,value)对的序列的方式提供访问,
因此,可以在map<string,int>
查找一个其int
大于42
的pair<const string,int>
:
void f(map<string,int>& m)
{
auto p = find_if(m.begin(),m.end(),Greater_than{42});
// ...
}
此处,Greater_than
是个函数对象(§6.3.2)持有42
以便用于比对操作:
struct Greater_than {
int val;
Greater_than(int v) : val{v} { }
bool operator()(const auto& r) const { return r.second>val; }
};
此外,还可以使用lambda表达式(§6.3.2):
auto p = find_if(m.begin(), m.end(), [](const auto& r) { return r.second>42; });
谓词不能对其访问的元素进行修改。
12.6 算法概览
算法的通用定义是 “由规则组成的有限集合,这些规则为解决一组特定问题规定一系列操作,(并)具有五个重要特性: 有限性……确定性……输入……输出……高效性” [Knuth,1968,§1.1]。 在C++标准库的语境里,算法是一个对元素序列执行操作的函数模板。
标准库提供了数十种算法。这些算法定义在命名空间std
中,
呈现在<algorithm>
头文件里。
这些标准库算法全都以序列作为输入。
一个从b
到e
的半开区间序列表示为[b
:e
)。
以下是几个范例:
部分标准库算法 <algorithm> |
|
---|---|
f=for_each(b,e,f) |
为[b :e )中的每个元素执行f(x) |
p=find(b,e,x) |
p 是[b :e )中第一个满足*p==x 的p |
p=find_if(b,e,f) |
p 是[b :e )中第一个满足f(*p) 的p |
n=count(b,e,x) |
n 是[b :e )中满足*q==x 的元素*q 的数量 |
n=count_if(b,e,f) |
n 是[b :e )中满足f(*q) 的元素*q 的数量 |
replace(b,e,v,v2) |
在[b :e )中用v2 替换满足*q==v 的元素*q |
replace_if(b,e,f,v2) |
在[b :e )中用v2 替换满足f(*q) 的元素*q |
p=copy(b,e,out) |
从[b :e )复制到[out :p ) |
p=copy_if(b,e,out,f) |
从[b :e )复制满足f(*q) 的元素*q 到[out :p ) |
p=move(b,e,out) |
从[b :e )移动到[out :p ) |
p=unique_copy(b,e,out) |
从[b :e )复制到[out :p );相邻的重复元素不复制 |
sort(b,e) |
以< 作为排序依据,对[b :e )中的元素进行排序 |
sort(b,e,f) |
以f 作为排序依据,对[b :e )中的元素进行排序 |
(p1,p2)=equal_range(b,e,v) |
[p1 :p2 )是有序序列[b :e )中值为v 的子序列;大体上就是针对v 的二分查找 |
部分标准库算法 <algorithm>(续表) |
|
---|---|
p=merge(b,e,b2,e2,out) |
把[b :e )和[b2 :e2 )两个有序序列和并进[out :p ) |
p=merge(b,e,b2,e2,out,f) |
把[b :e )和[b2 :e2 )两个有序序列和并进[out :p ),以f 作为比对依据 |
此处和许多其它的算法(见 §14.3),可应用于容器、string
以及内建数组的元素。
有些算法,例如replace()
和sort()
,修改元素值,
但不存在将容器元素增加或减少的算法。
原因是,一个序列并不知道持有此元素序列的是什么容器。
要增加或删除元素,你需要某个了解该容器的事物(比方 back_inserter
;§12.1)
或者直接在容器上进行操作(即 push_back()
或erase()
;§11.2)。
Lambda表达式经常作为操作以参数的形式传递,例如:
vector<int> v = {0,1,2,3,4,5};
for_each(v.begin(),v.end(),[](int& x){ x=x*x; }); // v=={0,1,4,9,16,25}
相较于手工书写的循环,标准库算法通常更谨慎、更有针对性地进行设计和实现, 因此,请了解并使用它们,以避免重复造轮子。
12.7 概束
在C++20中,标准库算法会被指定概束(第7章)。
相关的初期准备工作请参考 Ranges Technical Specification[RangesTS]。
其具体实现可以在互联网上找到。
对于C++20,区间这个概束定义在<ranges>
中。
Range
是针对 通过begin()
/end()
定义的C++98序列 的一个泛化。
Range
是个指定元素序列概念的概束。它的定义包括:
- 一个迭代器的
{begin,end}
对 - 一个
{begin,n}
对,其中begin
是个迭代器,n
是元素数量 - 一个
begin,pred
对,其中begin
是个迭代器,pred
是个谓词; 如果对于迭代器p
来说,pred(p)
为true
, 我们就到达了序列的末尾。 这给了我们数不清的序列,并且序列可以“随时按需(on the fly)”生成。
Range
这个概束让我们可以用sort(v)
取代sort(v.begin(),v.end())
,
后者是STL自1994年开始的使用方式。例如:
template<BoundedRange R>
requires Sortable<R>
void sort(R& r)
{
return sort(begin(r),end(r));
}
Sortable
的关系默认是less
。
一般来说,在标准库算法要求用一对迭代器表示某个序列的地方,
C++20就允许使用一个Range
作为简化的替代写法。
除Range
之外,C++20还提供许多便利的概束。
这些概束定义在头文件<ranges>
、<iterator>
和concepts
中。
核心语言概束<concepts> |
|
---|---|
Same<T,U> |
T 和U 是相同的类型 |
DerivedFrom<T,U> |
T 继承自U |
ConvertibleTo<T,U> |
一个T 可以转化成一个U |
CommonReference<T,U> |
T 和U 的共通引用类型相同 |
Common<T,U> |
T 和U 的共通类型相同 |
Integral<T> |
T 是个整数类型 |
SignedIntegral<T> |
T 是个有符号整数类型 |
UnsignedIntegral<T> |
T 是个无符号整数类型 |
Assignable<T,U> |
U 可以赋值给T |
SwappableWith<T,U> |
T 和U 可以被std:swap() |
Swappable<T> |
SwappableWith<T,T> |
对于某些算法,需要在应用于多个相关类型的时候具备数学上的合理性,
Common
在定义这些算法的时候就很重要。
Common<T,U>
是指某个类型C
,可以把T
和U
都先转换成C
进行比对。
例如,当我们可能想要把std::string
跟C-风格字符串(char*
),
或者把int
跟double
进行比对,但不会把std::string
和int
进行比对。
在用于定义Common
时,为确定common_type_t
的特化,适宜的方式为:
using common_type_t<std::string,char*> = std::string;
using common_type_t<double,int> = double;
Common
的定义略有点棘手,但解决了一个很难的基本问题。
幸运的是,除非需要进行操作的混合类型在库中(尚)无适当的定义,
我们无需为common_type_t
定义一个特化。
在定义那些需要对不同的类型做比对的概束和算法时,
多数都用到了Common
或CommonReference
。
与比对相关的概束受到了来自 [Stepanov,2009] 的重要影响:
比对相关的概束<concepts> |
|
---|---|
Boolean<T> |
T 可用作布尔类型(Boolean) |
WeaklyEqualityComparableWith<T,U> |
T 与U 可使用== 和!= 进行相等性比对 |
WeaklyEqualityComparable<T> |
WeaklyEqualityComparableWith<T,T> |
EqualityComparableWith<T,U> |
T 和U 可使用== 做等价性比对 |
EqualityComparable<T> |
EqualityComparableWith<T,T> |
StrictTotallyOrderedWith<T,U> |
T 和U 可使用
< 、<= 、
> 和>=
进行比对,得出全序关系
|
StrictTotallyOrdered<T> |
StrictTotallyOrderedWith<T,T> |
WeaklyEqualityComparableWith
和WeaklyEqualityComparable
二者的使用,
揭示了(到目前为止一直都)被忽视的重载机会。
对象概束<concepts> |
|
---|---|
Destructible<T> |
T 可被销毁且可用一元的& 获取其地址 |
Constructible<T,Args> |
T 可通过一个Args 类型的参数列表构造 |
DefaultConstructible<T> |
T 有默认构造函数 |
MoveConstructible<T> |
T 有转移构造函数 |
CopyConstructible<T> |
T 有拷贝构造函数和转移构造函数 |
Movable<T> |
MoveConstructible<T> 、
Assignable<T&,T> 和
Swapable<T>
|
Copyable<T> |
CopyConstructable<T> 、
Movable<T> 和
Assignable<T,const T&>
|
Semiregular<T> |
Copyable<T> 和
DefaultConstructable<T>
|
Regular<T> |
SemiRegular<T> 和
EqualityComparable<T>
|
Regular
是类型的理想状态。
Regular
类型用起来大体和int
差不多,
并且在某个类型的具体应用(§7.2)方面省却了许多操心。
类中默认==
的缺失,意味着多数类只能以SemiRegular
的形式面世,
尽管它们中的多数都本可以并应该成为Regular
。
可调用概束<concepts> |
|
---|---|
Invocable<F,Args> |
F 可通过一个Args 类型的参数列表调用 |
InvocableRegular<F,Args> |
F 可通过一个Args 类型的参数列表调用,并
维持等同性
|
Predicate<F,Args> |
F 可通过一个Args 类型的参数列表调用,返回bool值 |
Relation<F,T,U> |
Predicate<F,T,U> |
StrictWeakOrder<F,T,U> |
可确保
严格弱序
的Relation<F,T,U>
|
对于某个函数f()
,如果x==y
可导致f(x)==f(y)
,
则该函数是维持等同性(equality preserving)的。
严格弱序(strict weak ordering)是标准库针对顺序比对通常的假设,比如<
;
如果你觉得有必要了解就查一下(或者点击表格中该名称的链接——译者)。
Relation
和StrictWeakOrder
仅在语意上有所差别。
我们(目前)还无法在代码层面表示这一差异,因此这两个命名仅体现了我们的意图。
迭代器概束<iterators> |
|
---|---|
Iterator<I> |
I 可被++ 自增或* 解引用 |
Sentinel<S,I> |
S 是某个Iterator 类型的哨兵,
就是说,S 是个用于I 的值类型的谓词
|
SizedSentinel<S,I> |
S 是个哨兵,且可以用- 运算符和I 运算(即减法运算s-i ——译者) |
InputIterator<I> |
I 是个输入迭代器,* 可用于只读操作 |
OutputIterator<I> |
I 是个输出迭代器,* 可用于只写操作 |
ForwardIterator<I> |
I 是个前向迭代器,支持
multi-pass
|
BidirectionalIterator<I> |
I 是个ForwardIterator ,支持--
|
RandomAccessIterator<I> |
I 是个BidirectionalIterator ,
支持+ 、- 、+= 、-= 和[]
|
Permutable<I> |
I 是个ForwardIterator ,
并且I 支持移动和交换元素
|
Mergeable<I1,I2,R,O> |
可以按Relation<R> 把有序序列I2 和I2 合并入O
|
Sortable<I> |
可以按less 把承载I 的序列进行排序
|
Sortable<I,R> |
可以按Relation<R> 把承载I 的序列进行排序
|
对于给定的算法,迭代器的不同类型(分类)可用于选择最优的方式;
见 §7.2.2 和 §13.9.1。对于InputIterator
的范例,请参见 §12.4。
哨兵的基本思路是这样的:我们针对某个区间进行迭代,
该区间始自一个迭代器,直到谓词对于某个元素为 true 终止。
这样,一个迭代器p
和一个哨兵s
就定义了一个区间[p:s(*p)]
。
例如,为了遍历以指针作为迭代器的C-风格字符串,
可以为其哨兵定义一个谓词:
[](const char* p) {return *p==0; }
与C++20中的定义相比,此处Mergeable
和Sortable
的介绍进行了简化。
区间概束<ranges> |
|
---|---|
Range<R> |
R 是个区间,由一个起始迭代器和一个哨兵界定 |
SizedRange<R> |
R 是个区间,其size可在常数时间内获知 |
View<R> |
R 是个区间,其复制、移动和赋值的执行是常数时间 |
BoundedRange<R> |
R 是个区间,其迭代器和哨兵的类型一致 |
InputRange<R> |
R 是个区间,其迭代器类型符合 InputIterator 的要求 |
OutputRange<R> |
R 是个区间,其迭代器类型符合 OutputIterator 的要求 |
ForwardRange<R> |
R 是个区间,其迭代器类型符合 ForwardIterator 的要求 |
BidirectionalRange<R> |
R 是个区间,其迭代器类型符合 BidirectionalIterator 的要求 |
RandomAccessRange<R> |
R 是个区间,其迭代器类型符合 RandomAccessIterator 的要求 |
<ranges>
里还有几个其它的概束,但此表也够入门的了。
12.8 容器算法
在等不及 Range 概束的情况下,可以定义我们自己的简化版区间算法。
例如,提供sort(v)
这种简短写法取代sort(v.begin(),v.end())
可谓易如反掌:
namespace Estd {
using namespace std;
template<typename C>
void sort(C& c)
{
sort(c.begin(),c.end());
}
template<typename C, typename Pred>
void sort(C& c, Pred p)
{
sort(c.begin(),c.end(),p);
}
// ...
}
我把容器版本的sort()
(和其它算法)置于独有的命名空间Estd
(“extended std
”)中,以免跟其他程序员对命名空间std
的使用产生冲突,
同时还便于将来用Range
版本取代这个权宜之计。
12.9 并行算法
当对大量数据项执行同样的操作时,只要针对不同数据项的运算相互独立, 就能够以并行的方式去执行:
- 并行执行(parallel execution): 任务在多个线程上执行(通常运行在多个处理器核心上)
- 向量化执行(vectorized execution): 任务在单一线程上以向量化(vectorization)方式执行, 也称作SIMD(“Single Instruction, Multiple Data”)。
标准库对这两种都提供支持,还可以指定顺序执行,在<execution>
中有:
seq
:顺序执行par
:(在可行的情况下)并行执行par_unseq
:(在可行的情况下)并行执行 和/或 非顺序(向量化)执行。
以std::sort()
为例:
sort(v.begin(),v.end()); // 顺序执行
sort(seq,v.begin(),v.end()); // 顺序执行(与默认方式相同)
sort(par,v.begin(),v.end()); // 并行执行
sort(par_unseq,v.begin(),v.end()); // 并行执行 和/或 向量化执行
至于并行执行 和/或 向量化执行是否划算,要取决于算法、序列中元素的数量、硬件, 以及运行该程序的机器的利用率等等。 因此执行策略标志(execution policy indicators)仅仅是个示意(hint)。 编译器 和/或 运行时调度器将决定在多大程度上采用并发。 这并非无关紧要的小事,此外,“切勿未经测试就对性能下断言”的规则在此处尤为重要。
绝大多数标准库算法,包括 §12.6 表中除equal_range
外的全部,
都能像sort()
使用par
和par_unseq
那样被并行化和向量化。
为什么equal_range()
不行呢?因为到目前为止,它尚无有益的并行算法。
很多并行算法主要用于数值数据;参见 §14.3.1。
采用并行执行时,请确保避免数据竞争(§15.2)和死锁(§15.5)。
12.10 忠告
- [1] STL 算法可操作一个或多个序列;§12.1。
- [2] 输入序列是由一对迭代器定义的半开区间;§12.1。
- [3] 在进行查找时,算法通常返回输入序列的结尾以表示“未找到”;§12.2。
- [4] 算法不会直接在其参数序列中增添或删减元素;§12.2,§12.6。
- [5] 写循环时,考虑一下能否以一个通用算法实现;§12.2。
- [6] 请使用谓词和其它函数对象为标准算法赋予更广泛的意义;§12.5,§12.6。
- [7] 谓词绝不可修改其参数;§12.5。
- [8] 请通晓标准库算法,并用其代替手写的循环;§12.6。
- [9] 在 迭代器对 的风格显得冗长时,可以引入一个 容器/区间 版本的算法;§12.8。
1. 著名的“奥卡姆剃刀”法则,此处作者可能误会了说这短引言作者的名字,因为其名字应该是“William of Occam”,意思是“奥卡姆这个地方的威廉”,作者的写法里“奥卡姆”是他的姓。该名词有专门的的维基百科页面 —— 译者注 ↩