multimap

multimap 是一种允许多个元素使用同一个键的 map。和 map 一样,multimap 支持统一初始化。multimap的接口和 map 的接口几乎相同,区别在于multimap 不提供 operator[]和 at()。它们的语义在多个元素可以使用同一个键的情况下没有意义。在 multimap 上执行插入操作总是会成功。因此,添加单个元素的 multimap::insert()方法只返回 iterator而不返回 pair。map 支持 insert_or_assign()和 try_emplace()方法,而 multimap 不支持。multimap 允许插入相同的键值对。如果要避免这种宛余,必须在插入新元素之前执行显式检查。multimap 的最环手之处是查找元素。不能使用 operator[],因为并没有提供 operator[]。find()也不是非常有用,因为 find()返回的是指向具有给定键的任意一个元素的 iterator(未必是具有这个键的第一个元素)。然而,multimap 将所有带同一个键的元素保存在一起,并提供方法以获得这个子范围的 iterator,这个子范 围内的元素在容器中具有相同的键。lower_bound()和 upper_bound()方法分别返回匹配给定键的第一个元素和最后一个元素之后那个元素(one-past-the-lasb)的对应 iterator。如果没有元素匹配这个键,那么 lower_bound()和upper_bound()返回的 iterator 相等。如果需要获得具有给定键的元素对应的 iterator,使用 equal range()方法比依次调用 lower_bound()和upper_bound()更高效.equal_range()返回两个iterator 的 pair, 这两个 iterator 分别是 lower_bound()和 upper_bound()返回的 iterator。

注意:

” map 中也有 lower_ boundO0、upper bound0和 equal_range()方法,但由于 map 中不允许多个元素带有同一个键,因此在 map 中,这些方法的用处不大。

Multimap 示例: 好友列表

大部分在线聊天软件都允许用户有一个“好友列表”。聊天软件给好友列表中的用户赋予特殊权限,例如允许他们向用户发送未经请求的消息。在线聊天软件实现好友列表的一种方式是将信息保存在 multimap 中。一个 multimap 可保存每个用户的好友列表。容器中的每一项保存用户的一个好友。键是用户,值是好友。例如,如果 Harry Potter 和 Ron Weasley都出现在对方的好友列表中, 那么应该有两项,一项将 Harry Potter 映射到Ron Weasley,另一项将 Ron Weasley贞射到 Harry Potter。multimap 允许同一个键有多个值,因此同一个用户允许有多个好友。下面是 BuddyList 类的定义:

class BuddyList final
{public://Adds buddy as a friend of namevoid addBuddy(const std::string& name,const std::string& buddy);//Removes Buddy as a friend of namevoid removeBuddy(const std::string& name,const std::string& buddy);//Returns true if buddy is a friend of name false otherwisebool isBuddy(const std::string& name,const std::string& buddy) const;std::vector<std::string> getBuddies(const std::string& name) const;private:std::multimap<std::string, std::string> mBuddies;
};

下面是这个类的实现,其中包含代码注释。这个实现演示了 lower bound()、upper_bound()和 equal range()的用法:


void BuddyList::addBuddy(const string& name,const string& buddy)
{//Make sure this buddy isn't already there,we don't want//to insert an identical copy of the key/value pairif(!isBuddy(name,buddy)){mBuddies.insert({name,buddy});//Using initializer_list}
}void BuddyList::removeBuddy(const string& name,const string& buddy)
{//Obtain the begining and end of the range of elements with//key 'name', Use both lower_bound() and upper_bound() to demonstrate//their use,Otherwise it's more efficient to call equal_range()auto begin = mBuddies.lower_bound(name); //Start of the rangeauto end = mBuddies.upper_bound(name); //End of the range//Iterate through the elemente with key 'name' looking//for a value 'buddy' if there are no elements with key 'name'//being equals end,so the loop body doesn't executefor(auto iter = begin; iter != end; ++iter){if(iter->second == buddy){//we found a match! Remove it from the mapmBuddies.erase(iter);break;}}
}
bool BuddyList::isBuddy(const string& name,const string& buddy) const
{//Obtain the begining and end of the range of elements with//key 'name' using equal_range(),and c++17 structured bindingsauto [begin,end] = mBuddies.equal_range(name);//Iterate through the elements with key 'name' looking//for a value 'buddy'for(auto iter = begin;iter!=end;++iter){if(iter->second == buddy){//We found a matchreturn true;}}//No matchesreturn false;
}vector<string> BuddyList::getBuddies(const string& name) const
{//Obtain the begining and end of the range of elements with//key 'name' using equal_range(),and c++ 17 structrued bingingsauto[begin,end] = mBuddies.equal_range(name);//Create a vector with all names int the range (all buddies of name)vector<string> buddies;for(auto iter = begin;iter!=end;++iter)   {buddies.push_back(iter->second);}return buddies;
}

该实现使用了 C++17 结构化绑定,如下所示:

auto[begin,end] = mBuddies.equal_range(name);

如果编译器尚不支持结构化绑定,可编写如下代码:

auto range = mBuddies.equal_range(name);
auto begin = range.first; //start of the range
auto end = range.second; //end of the range

注意,removeBuddy()不能使用删除具有给定键的所有元素的那个 erase()版本,它只应删除具有指定键的一个元素, 而不是删除具有指定键的所有元素。还要注意, getBuddies()不能在 vector 上通过 insert()向 equal_range()返回的范围插入元素, 因为 multimap 友代器引用的元素是键值对而不是字符串。getBuddies(方法必须显式地志历范围,将字符串从每一个键值 pair 中抽取出来,然后插入要返回的新 vector。下面是对 BuddyList 的测试

int main()
{BuddyList buddies;buddies.addBuddy("test1","buddy1");buddies.addBuddy("test1","buddy2");buddies.addBuddy("test1","buddy3");buddies.addBuddy("test1","buddy4");//That's not right! remove buddy4buddies.removeBuddy("test1","buddy4");buddies.addBuddy("test2","buddy5");buddies.addBuddy("test2","buddy1");buddies.addBuddy("test2","buddy2");auto test1Friends = buddies.getBuddies("test1");cout<<"test1's friends: "<<endl;for(const auto & name : test1Friends){cout<<"\t"<<name<<endl;}return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
test1's friends: buddy1buddy2buddy3

set

set 容器定义在头文件中,和 map 非常类似。区别在于 set 保存的不是键值对,在 set 中,值本身就是键。如果信息没有显式的键,且希望进行排序(不包含重复)以便快速地执行插入、查找和删除,就可以考虑使用 set 容器来存储此类信息。set 提供的接口几乎和 map 提供的接口完全相同,主要区别在于 set 没有提供 operator[]、insert_or_assign()和 try_emplace()。不能修改 set 中元素的键/值,因为修改容器中的 set 元素会破坏顺序。

set 示例: 访问控制列表在计算机系统上实现基本安全控制的一种方法是使用访问控制列表。系统上的每个实体(如文件和设备)都有一个用户列表,列出了有权访问相应实体的用户。通常只有拥有特殊权限的用户才能在实体的访问权限列表中添加和删除用户。在系统内部,set 容器可以很好地表示访问控制列表。每个实体可以使用一个 set,其中包含所有人允许访问这个实体的用户名。下面是这个简单访问控制列表的类定义:

class AccessList final
{public://Default constructorAccessList() = default;//Constructor to support uniform initializationAccessList(std::initializer_list<std::string_view> initlist);//Adds the user to the permissions listvoid addUser(std::string_view user);//Returns true if the user is in the permission listbool isAllowed(std::string_view user) const;//Removes the user from the permissions listvoid removeUser(std::string_view user);//Returns a vector of all the users who have permissionsstd::vector<std::string> getAllUsers() const;private:std::set<std::string> mAllowed;
};

下面是方法的定义:

AccessList::AccessList(initializer_list<string_view> initlist)
{mAllowed.insert(begin(initlist),end(initlist));
}void AccessList::addUser(string_view user)
{mAllowed.emplace(user);
}
void AccessList::removeUser(string_view user)
{mAllowed.erase(string(user));
}
bool AccessList::isAllowed(string_view user) const
{return (mAllowed.count(string(user)) != 0);
}
vector<string> AccessList::getAllUsers() const
{return {begin(mAllowed),end(mAllowed)};
}

getAllUsers()的这行实现十分有趣,有必要分析一下。将这一行构建的 vector返回给 vector 构造函数,return 的参数是 mAllowed 的首尾迭代器。如有必要,可将其分为两行:

vector<string> users(begin(mAllowed),end(mAllowed));
return users;

下面是一个简单的测试程序:

AccessList fileX = {"pvw","mgregoire","baduser"};
fileX.removeUser("baduser");if(fileX.isAllowed("mgregoire"))
{cout<<"mgregoire has permissions"<<endl;
}
if(fileX.isAllowed("baduser"))
{cout<<"baduser has permissions"<<endl;
}
auto users = fileX.getAllUsers();
for(const auto& user : users)
{cout<<user<<" ";
}

输出

xz@xiaqiu:~/study/test/test$ ./test
mgregoire has permissions
mgregoire pvw
xz@xiaqiu:~/study/test/test$

无序关联容器/哈希表

标准库支持无序关联容器或哈希表。这种容器有 4 个: unordered_map、unordered_multimap、unordered _set和 unordered_multiset。此前讨论的 map、multimap、set 和 multiset 容器对元素进行排序,而这些新的无序版本不会对元素进行排序。

哈希函数

无序关联容器也称为哈希表,这是因为它们使用了哈希函数(hash function)。哈希表的实现通常会使用某种形式的数组,数组中的每个元素都称为桶(bucket)。每个桶都有一个特定的数值索引,例如 0、1、2 直到最后一个桶。哈希函数将键转换为哈希值,再转换为桶索引。与这个键关联的值在桶中存储。

哈希函数的结果未必是唯一的。两个或多个键哈希到同一个桶索引,就称为冲突(collision)。当使用不同的键得到相同的哈希值,或把不同的哈希值转换为同一桶索引时,会发生冲突。可采用多种方法来处理冲突,例如二次重哈希(quadratic re-hashing)和线性链(linear chaining)等方法。感兴趣的读者可参阅附录 B 中“算法和数据结构”部分列出的任意参考文献。标准库没有指定要求使用哪种冲突处理算法,但目前大部分实现都选择通过线性链解决冲突。使用线性链时,桶不直接包含与键关联的数据值,而包含一个指向链表的指针。这个链表包含特定桶中的所有数据值。图 17-1 展示了原理。图 17-1 中有两个冲突。之所以出现第一个冲突,是因为对键“Marc G”和“John D.”应用哈希函数后得到同一个哈希值,该哈希值被映射到桶索引 128。这个桶指向一个包含键“Marc G”和“John D.”及其对应数据值的链表。第二个冲突由“Scott K.”和“Johan G”的哈希值引起,它们被映射到相同的桶索引 129。

从图 17-1 中还可看出基于键的查找的工作原理以及查找的复杂度。 查找过程包括调用一次哈希函数来计算哈希值,哈希值此后被转换为桶索引。一旦知道了桶索引,, 将在链表中通过一次或多次相等操作找到正确的键。从中还能看出,相比普通 map 的查找方式,这种查找方式要快得多,但查找速度完全取决于冲突次数。哈希函数的选择非常重要。不产生冲突的哈希函数称为“完美哈希*。完美哈希的查找时间是常量; 常规的哈希查找时间平均接近于 1,与元素数量无关。随着冲突数的增加,查找时间会增加,性能会降低。增加基本哈希表的大小,可以减少冲突,但需要考虑高速缓存的大小。

C++标准为指针和所有基本数据类型(例如 bool、 char, int float double 等)提供了哈希函数, 还为 error_code、error_condition、optional、variant、bitset、unique_ptr、shared_ptr、type_index、string、string_view、Yvector和 thread::id 提供了哈希函数。如果要使用的键类型没有可用的标准哈希函数,就必须实现自己的哈希函数。即使键集是固定的、已知的,创建完美哈希也并不简单,需要进行深入的数学分析。纵然创建得不算完美,但性能较高,仍然充满挑战。由于篇幅所限,本书不详细解释哈希函数的数学原理,只会列举一个十分简单的哈希

函数示例。

下面的示例演示了如何编写自定义哈希函数。这个示例仅将请求传递给可用的一个标准哈希函数。代码定义了一个类 IntWrapper,它仅封装了一个整数。还提供了 operator==,因为这是在无效关联容器中使用键所必需的

class IntWrapper
{public:IntWrapper(int i) : mWrappedInt(i){}    int getValue() const{ return mWrappedInt; }private:int mWrappedInt;
};bool operator==(const IntWrapper& lhs,const IntWrapper& rhs)
{return lhs.getValue() == rhs.getValue();
}

为给inttWrapper 编写哈希函数,应给 IntWrapper 编写 std::hash 模板的特例。std::hash 模板在中定义。这个特例需要实现函数调用运算符,以计算并返回给定 intWrapper 实例的哈希。对于这个示例,仅把请求传递给整数的标准哈希函数:

namespace std
{template<> struct hash<IntWrapper>{using argument_type = IntWrapper;using result_type = size_t;retult_type operator()(const argument_type& f) const{return std::hash<int>()(f.getValue());}};
}

注意一般不允许把任何内容放在 std 名称空间中, 但 std 类模板特例是这条规则的例外。hash 类模板需要两个类型定义。函数调用运算符的实现只有一行代码,它为整数的标准哈希函数创建了一个实例 std::hash(),然后对该实例通过参数 fgetValue()执行函数调用运算符。注意这个传递在本例中是有效的,因为 intWrapper 只包含一个数据成员: 一个整数。如果该类包含多个数据成员,就需要在计算哈希时考虑所有数据成员,但这些细节超出了本书的讨论范围。

unordered_ map

unordered_map 容器在<unordered_map>头文件中定义,也是一个类模板,如下所示:

template <class Key,class T,class Hash = hash<Key>,class Pred = std::equal_to<Key>,class Alloc = std::allocator<std::pair<const key,T>>>class unordered_map;

共有 5 个模板参数,键类型、值类型、哈希类型、判等比较类型和分配器类型。通过后面 3 个参数可以分别自定义哈希函数、判等比较函数和分配器函数。通常可名略这些参数,因为它们有默认值。建议保留默认值。最重要的参数是前两个参数。与 map 一样,可使用统一初始化机制来初始化 unordered_map,如下所示:

unoredered_map<int,string>m = {{1,"Item 1"},
{2,"Item 2"},
{3,"Item 3"},
{4,"Item 4"}
};//Using c++17 structured bindings
for(const auto&[key,value]:m)
{cout<<key<<"="<<value<<endl;
}
//without structured bindings
for(const auto & p : m)
{cout<<p.first<<"="<<p.second<<endl;
}

与普通的 map 一样,unordered_map 中的所有键都应该是唯一的。表 17-5 中包含一些哈希专用方法。例如,load _ factor()返回每一个桶的平均元素数,以反映冲突的次数。bucket_count()方法返回容器中桶的数目。还提供了 local iterator 和 const_local iterator,用于遍历单个桶中的元素,但不能用于遍历多个桶。bucket(key)方法返回包含指定键的桶索引,begin(n)返回引用索引为n 的桶中第一个元素的 local_iterator,end(n)返回引用索引为n的桶中最后一个元素之后那个元素(one-past-the-lasb)的 local iterator。下面的例子将演示这些方法的用法。

unordered_map 示例: 电话簿

下例通过 unordered_map 来表示电话本。使用人名来表示键,电话号码则是与键关联的值。

template<class T>
void printMap(const T& m)
{for(auto& [key,value] : m){cout<<key<<" (Phone: "<<value<<")"<<endl;}cout<<"------------------"<<endl;
}
int main()
{//Create a hash tableunordered_map<string,string> phoneBook = {{"Marc G","123-456789"},{"Scott K","654-233133"}};printMap(phoneBook);//Add/Remove some phone numbersphoneBook.insert(make_pair("John D","321-987654"));phoneBook["Johan G."] = "963-258147";phoneBook["Freddy K."] = "999-256256";printMap(phoneBook);//Find the bucket index for a specific keyconst size_t bucket = phoneBook.bucket("Marc G.");cout<<"Marc G.is in bucket "<<bucket<<" which contains the following "<<phoneBook.bucket_size(bucket)<<" element: "<<endl;//Get begin and end iterators for the elements in this bucket//'auto' is used here,The compiler deduces the type of//both as unodered_map<string,string>::const_local_iteratorauto localBegin = phoneBook.cbegin(bucket);auto localEnd = phoneBook.cend(bucket);for(auto iter = localBegin;iter != localEnd;++iter){cout<<"\t"<<iter->first<<" (Phone: "<<iter->second<<" ) "<<endl;}cout<<"--------------"<<endl;//Print some statistics about the hash tablecout<<"There are "<<phoneBook.bucket_count()<<" buckets."<<endl;cout<<"Average number of elements in a bucket is "<<phoneBook.load_factor()<<endl;return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Scott K (Phone: 654-233133)
Marc G (Phone: 123-456789)
------------------
Freddy K. (Phone: 999-256256)
Marc G (Phone: 123-456789)
Johan G. (Phone: 963-258147)
John D (Phone: 321-987654)
Scott K (Phone: 654-233133)
------------------
Marc G.is in bucket 3 which contains the following 3 element: Johan G. (Phone: 963-258147 ) John D (Phone: 321-987654 ) Scott K (Phone: 654-233133 )
--------------
There are 5 buckets.
Average number of elements in a bucket is 1

unordered_multimap

unordered_ multimap 是允许多个元素带有同一个键的 unordered map。两者的接口几乎相同,区别在于:unordered multimap 没有提供 operator[]运算符和 at(),它们的语义在多个元素可以使用同一个键的情况下没有意义。在 unordered multimap 上执行插入操作总是会成功。因此,添加单个元素的 unordered_ multimap::insert()方法只返回迭代器而非 pair。 unordered_map 支持 insert_or_assign()和 try_emplace()方法,而以nordered_multimap 不支持这两个方法。

注意:

unordered_ multimap 多许插入相同的键值对。如果想要避免这种宛余,必须在插入新元素之前执行显式的检查。根据之前对 multimap 的描述,不能使用 operator[]运算符在 unordered_multimap 中查找元素,因为没有提供这个运算符。find()虽然可供使用,但它返回的是引用具有给定键的任意一个元素的迭代器(未必是具有这个键的第一个元素)。最好使用 equal_range()方法,它返回两个迭代器的 pair: 一个引用匹配给定键的第一个元素,另一个引用匹配给定键的最后一个元素之后的那个元素(one-past- the-lasb。equal rangeO)的用法和之前讨论multimap 的 equal_range(0)完全一样,因此可以参考 multimap 的示例来了解 equal_range0的工作方式。

unordered_set/unordered_multiset

头文件定义了 unordered set 和 unordered multiset,这两者分别类似于 set 和 multiset;,区别在于它们不会对键进行排序,而且使用了哈希函数。unordered_set 和 unordered_map 的区别和之前讨论的 set 和 map之间的区别类似,因此这里不再歼述。标准库参考资源完整总结了 unordered set 和 unordered_multiset 操作 。

其他容器

C++语言中还有其他一些可在不同程度上与标准库合作的部分,包括标准 C 风格数组、string、流和 bitset。

标准 C 风格数组

回顾一下,普通指针也算是欠代器,因为它们支持所需的运算符。这一点并不是琐碎的小知识。它意味着可以把标准 C 风格数组看成标准库容器,只要把指向数组元素的指针当成迭代器即可。当然,标准 C 风格数组并没有提供 size()、empty()、insert()和 erase()这类方法,因此它们并非真正的标准库容器。不管怎么样,它们通过指针的方式支持迭代器,因此可在第 18 章描述的算法和本章描述的一些方法中使用它们。

例如,可通过 vector 中接收任何容器迭代器范围的 insert()方法,将标准 C 风格数组中的所有元素复制到vector 中。这个 insert()方法的原型如下所示:

template<class InputIterator> iterator insert(const_iterator position,inputIterator first,inputIterator last);

如果想用标准的 C 风格 int 数组作为数据来源,那么将 inputIterator 的模板化类型蔡换为int*。下面是完整的例子:

const size_t count = 10;
int arr[count]; //standard C-style array
//Initialize each element of the array to the value of its index
for(int i = 0;i < count;i++)
{arr[i] = i;
}
//Insert the contents of the array at the end of a vector
vector<int> vec;
vec.insert(end(vec),arr,arr+count);//print the contents of the vector
for(const auto& i : vec)
{cout<<i<<" ";
}

注意,引用数组中第一个元素的迭代器是第一个元素的地址,也就是这个例子中的 arr。数组名字本身可解释为第一个元素的地址。引用尾部的迭代器必须引用最后一个元素之后的那个元素(one-past-the-last element),因此这是第一个元素加 count 的地址,即 arr+count。很容易使用 std::begin()或 std::cbegin()获得指向静态分配的 C 风格数组(不通过指针访问)中第一个元素的迭代器,使用 std::end()或 std::cend()获得此类数组中最后一个元素之后那个元素的迭代器。例如,前面示例中对insert()的调用可以写为:

vec.insert(end(vec),cbegin(arr),cend(arr));

警告:

std::begin()和std::end()等函数仅用于静态分配的 C 风格数组(不通过指针访问)。如果涉及指针或使用动态分配的 C 风格数组,则不可行。

string

可将 string 看成字符的顺序容器。因此,C++ string 实际上是一种功能完备的顺序容器。string 包含的 begin()和 end()方法返回 string 中的返代器,还包含 insert()、push_back()、erase()、size()和 empty()方法,以及基本顺序容器包含的其他所有内容。string 非常接近于 vector,甚至还提供了 reserve()和 capacity()方法。可以像使用 vector 那样将 string 作为标准库容器使用。下面是一个例子:

string myString;
myString.insert(myString,'h');
myString.insert(myString,'e');
myString.push_back('l');
myString.push_back('l');
myString.push_back('o');for(const auto& letter : myString)
{cout<<letter;
}
cout<<endl;

除了标准库顺序容器方法外, string 还提供了很多有用的方法和友元函数。第 2 章更详细地讨论了 string 类。

传统意义上,输入流和输出流并不是容器,因为它们并不保存元素。然而,可以把它们看成元素的序列,因而具有标准库容器的一些特性。C++流没有直接提供与标准库相关的任何方法,但是标准库提供了名为istream_iterator 和 ostream_iterator 的特殊迭代器,用于“遍历”输入流和输出流。第 21 章将讲解这些迭代器的用法。

bitset

bitset 是固定长度的位序列的抽象。 一个位只能表示两个值一1 和 0, 这两个值可以表示开/关和真/假等意义。bitset 还使用了设置(cet)和清零(unset)两个术语。可将一个位从一个值切换(toggle)或翻转(flip)为另一个值。bitset 并不是真正的标准库容器: bitset 的大小固定,没有对元素类型进行模板化,也不支持迭代。然而,这是一个有用的工具类,而且常和容器在一起,因此这里做一下简要介绍。标准库参考资源对 bitset 操作做了全面总结。

  1. bitset 基础

bitset 定义在头文件中, 根据保存的位数进行模板化。默认构造函数将 bitset 的所有字段初始化为 0。另一个构造函数根据由0 和 1 字符组成的字符串创建 bitset。可通过 set()、reset()和 生p()方法改变单个位的值, 通过重载的 operator[]运算符可访问和设置单个字段的值。注意对非 const 对象应用 operator[]会返回一个代理对象,可为这个代理对象赋予一个布尔值,调用 flip()或~取反。还可通过 test()方法访问单独字段。此外,通过普通的插入和抽取运算符可以流式处理 bitset。bitset 以包含0和 1 字符的字符串形式进行流式处理。下面是一个简单例子,

bitset<10>myBitset;
myBitset.set(3);
myBitset.set(6);
myBitset[8] = true;
myBitset[9] = myBitset(3);
if(myBitset.test(3))
{cout<<"Bit 3 is set!"<<endl;
}
cout<<myBitset<<endl;

输出为;

Bit 3 is set!
1101001000

注意所输出字符串的最左边字符表示最高位。 这符合我们对二进制数表示方式的直观看法,表示 2"-1 的最低位出现在印刷表示方式的最右位。

按位运算符

除基本的位操作外,bitset 还实现了所有的按位运算符,&、|、、~、<<、>>、&=、|=、=、<<=和>>=。这些运算符的行为和操作真正的位序列相同。下面举一个例子:

auto str1 = "0011001100";
auto str2 = "0000111100";
bitset<10> bitsOne(str1);
bitset<10> bitsTwo(str2);
auto bitsThree = bitsOne & bitsTwo;
cout << bitsThree << endl;
bitsThree <<= 4;
cout << bitsThree << endl;

输出

0000001100
0011000000

bitset 示例: 表示有线电视频道

bitset 的一种可能应用是跟踪有线电视用户的频道。每个用户都有一组用 bitset 表示的频道,这个 bitset 与用户的订阅情况相关,设置的位表示用户实际订阅的频道。这个系统还可以支持频道“套餐”,套餐也表示为bitset,通过 bitset 表示常用的频道组合。下面的 CableCompany 类是这个模型的简单示例。这个类使用了两个 map,它们都是 string/bitset 的 map,保存了有线频道套餐和用户信息。

const size_t kNumChannels = 10;
class CableCompany final
{public:// Adds the package with the specified channels to the database.void addPackage(std::string_view packageName, const std::bitset<kNumChannels> &channels);// Removes the specified package from the database.void removePackage(std::string_view packageName);// Retrieves the channels of a given package.// Throws out_of_range if the package name is invalid.const std::bitset<kNumChannels> &getPackage(std::string_view packageName) const;// Adds customer to database with initial channels found in package.// Throws out_of_range if the package name is invalid.// Throws invalid_argument if the customer is already known.void newCustomer(std::string_view name, std::string_view package);// Adds customer to database with given initial channels.// Throws invalid_argument if the customer is already known.void newCustomer(std::string_view name,const std::bitset<kNumChannels> &channels);// Adds the channel to the customers profile.// Throws invalid_argument if the customer is unknown.void addChannel(std::string_view name, int channel);// Removes the channel from the customers profile.// Throws invalid_argument if the customer is unknown.void removeChannel(std::string_view name, int channel);// Adds the specified package to the customers profile.// Throws out_of_range if the package name is invalid.// Throws invalid_argument if the customer is unknown.void addPackageToCustomer(std::string_view name,std::string_view package);// Removes the specified customer from the database.void deleteCustomer(std::string_view name);// Retrieves the channels to which a customer subscribes.// Throws invalid_argument if the customer is unknown.const std::bitset<kNumChannels> &getCustomerChannels(std::string_view name) const;
private:// Retrieves the channels for a customer. (non-const)// Throws invalid_argument if the customer is unknown.std::bitset<kNumChannels> &getCustomerChannelsHelper(std::string_view name);using MapType = std::map<std::string, std::bitset<kNumChannels>>;MapType mPackages, mCustomers;
};

下面是上述方法的实现,其中包含代码注释:

void CableCompany::addPackage(string_view packageName,const bitset<kNumChannels> &channels)
{mPackages.emplace(packageName, channels);
}
void CableCompany::removePackage(string_view packageName)
{mPackages.erase(packageName.data());
}
const bitset<kNumChannels> &CableCompany::getPackage(string_view packageName) const
{// Get a reference to the specified package.auto it = mPackages.find(packageName.data());if (it == end(mPackages)){// That package doesn't exist. Throw an exception.throw out_of_range("Invalid package");}return it->second;
}
void CableCompany::newCustomer(string_view name, string_view package)
{// Get the channels for the given package.auto &packageChannels = getPackage(package);
// Create the account with the bitset representing that package.newCustomer(name, packageChannels);
}
void CableCompany::newCustomer(string_view name,const bitset<kNumChannels> &channels)
{// Add customer to the customers map.auto result = mCustomers.emplace(name, channels);if (!result.second){// Customer was already in the database. Nothing changed.throw invalid_argument("Duplicate customer");}
}
void CableCompany::addChannel(string_view name, int channel)
{// Get the current channels for the customer.auto &customerChannels = getCustomerChannelsHelper(name);
// We found the customer; set the channel.customerChannels.set(channel);
}
void CableCompany::removeChannel(string_view name, int channel)
{// Get the current channels for the customer.auto &customerChannels = getCustomerChannelsHelper(name);
// We found this customer; remove the channel.customerChannels.reset(channel);
}
void CableCompany::addPackageToCustomer(string_view name, string_view package)
{// Get the channels for the given package.auto &packageChannels = getPackage(package);
// Get the current channels for the customer.auto &customerChannels = getCustomerChannelsHelper(name);
// Or-in the package to the customer's existing channels.customerChannels |= packageChannels;
}
void CableCompany::deleteCustomer(string_view name)
{mCustomers.erase(name.data());
}
const bitset<kNumChannels> &CableCompany::getCustomerChannels(string_view name) const
{// Use const_cast() to forward to getCustomerChannelsHelper()
// to avoid code duplication.return const_cast<CableCompany *>(this)->getCustomerChannelsHelper(name);
}
bitset<kNumChannels> &CableCompany::getCustomerChannelsHelper(string_view name)
{// Find a reference to the customer.auto it = mCustomers.find(name.data());if (it == end(mCustomers)){throw invalid_argument("Unknown customer");}
// Found it.
// Note that 'it' is a reference to a name/bitset pair.
// The bitset is the second field.return it->second;
}

最后,下面这个简单程序演示了如何使用 CableCompany 类:

int main()
{CableCompany myCC;auto basic_pkg = "1111000000";auto premium_pkg = "1111111111";auto sports_pkg = "0000100111";myCC.addPackage("basic", bitset<kNumChannels>(basic_pkg));myCC.addPackage("premium", bitset<kNumChannels>(premium_pkg));myCC.addPackage("sports", bitset<kNumChannels>(sports_pkg));myCC.newCustomer("Marc G.", "basic");myCC.addPackageToCustomer("Marc G.", "sports");cout << myCC.getCustomerChannels("Marc G.") << endl;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
1111100111
xz@xiaqiu:~/study/test/test$

掌握标准库算法

由第 17 章可知,标准库提供了大量泛型数据结构。大部分库都只提供数据结构。标准库却包含了大量泛型算法,这些算法大部分(只有少部分例外)都可以应用于任何容器的元素。通过这些算法,可在容器中查找、排序和处理元素,并执行其他大量操作。算法之美在于算法不仅独立于底层元素的类型,而且独立于操作的容器的类型。算法仅使用迭代器接口执行操作。大部分算法都接受回调(callback),回调可以是函数指针,也可以是行为类似于函数指针的对象,例如重载了运算符 operator(0的对象或内嵌的 lambda 表达式。重载 operator0的类称为函数对象或仿函数(functon。为方便起见,标准库还提供了一组类,用于创建算法使用的回调对象。

算法概述

算法的魔力在于,算法把迭代器作为中介操作容器,而不直接操作容器本身。这样,算法没有绑定至特定的容器实现。所有标准库算法都实现为函数模板的形式,其中模板类型参数一般都是迭代器类型。将迭代器本身指定为函数的参数。模板化的函数通常可通过函数参数推导出模板类型,因此通常情况下可以像调用普通函数(而非模板)那样调用算法。迭代器参数通常都是迭代器范围。根据第 17 章的描述,对于大部分容器来说,迭代器范围都是半开区间,因此包含范围内的第一个元素,但不包括最后一个元素。尾迭代器实际上是跨越最后一个元素(past-the-end)的标记。算法对传递给它的迭代器有一些要求。例如,copy_backward()需要双向迭代器,stable_sort()需要随机访问

find()和 find_if()算法

find()在某个迭代器范围内查找特定元素。可将其用于任意容器类型的元素。这个算法返回引用所找到元素的迭代器,如果没有找到元素,则返回迭代器范围的尾迭代器。注意调用 find()时指定的范围不要求是容器中元素的完整范围,还可以是元素的子集。

警告:

如果 find()没有找到元素,那么返回的选代器等于函数调用中指定的尾选代器,而不是底层容器的尾和迭代器。下面是一个 std::find()示例。注意这个示例假定用户正常操作,输入的是合法数值; 这个程序不会对用户输入执行任何错误检查。第 13 章讨论了如何对流式输入执行错误检查。

#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
int main()
{int num;vector<int> myVector;while(true){cout<<"Enter a number to add (0 to stop): ";cin>>num;if(num == 0){break;}myVector.push_back(num);}while(true){cout<<"Enter a number to lookup (0 to stop): ";cin>>num;if(num == 0){break;}auto endIt =  cend(myVector);auto it = find(cbegin(myVector),endIt,num);if(it == endIt){cout<<"Could not find "<<num<<endl;}else{cout<<"Found "<<*it<<endl;}}return 0;
}

调用 find()时将 cbegin(myVector)和 endit 作为参数, 其中,endlt 定义为 cend(myVector), 因此搜索的是 vector的所有元素。如果需要搜索一个子范围,可修改这两个迭代器。下面是运行这个程序的示例输出:

xz@xiaqiu:~/study/test/test$ ./test
Enter a number to add (0 to stop): 1
Enter a number to add (0 to stop): 2
Enter a number to add (0 to stop): 3
Enter a number to add (0 to stop): 43
Enter a number to add (0 to stop): 213
Enter a number to add (0 to stop): 12
Enter a number to add (0 to stop): 321
Enter a number to add (0 to stop): 1
Enter a number to add (0 to stop): 2
Enter a number to add (0 to stop): 0
Enter a number to lookup (0 to stop): 1
Found 1
Enter a number to lookup (0 to stop): 2
Found 2
Enter a number to lookup (0 to stop): 3
Found 3
Enter a number to lookup (0 to stop): 12
Found 12
Enter a number to lookup (0 to stop): 32131
Could not find 32131
Enter a number to lookup (0 to stop):

使用主语句的初始化器(C++17),可使用如下加粗语句来调用 find()并查找结果:

if(auto it = find(cbegin(myVector),endIt,num);it == endIt)
{cout<<"Could not find "<<num<<endl;
}
else
{cout<<"Found "<<*it<<endl;
}

一些容器(例如 map 和 set类方法的方式提供自己的 find()版本。

警告:

如果容器提供的方法具有与泛型算法同样的功能,那么应该使用相应的方法,那样速度更快。比如,泛型算法 find()的复杂度为线性时间,用于 map 和迭代器时也是如此; 而map 中find()方法的复杂度是对数时间。find_if()和 find()类似,区别在于 find_if()接收谓词函数回调作为参数,而不是简单的匹配元素。谓词返回true 或 false。find_if()算法对范围内的每个元素调用谓词,直到谓词返回 true;, 如果返回了 true,find_if()返回引用这个元素的友代器。下面的程序从用户读入测试分数,检查是否存在“完美”分数。完美分数指的是大于或等于 100 的分数。这个程序与前一个例子中的程序相似。两个程序的区别已加粗显示。

bool perfectScore(int num)
{return (num >= 100);
}
int main()
{int num;vector<int> myVector;while(true){cout<<"Enter a test score to add (0 to stop): ";cin>>num;if(num == 0){break;}myVector.push_back(num);}auto endIt = cend(myVector);auto it = find_if(cbegin(myVector),endIt,perfectScore);if(if == endIt){cout<<"No perfect scores"<<endl;}else{cout<<"Found a \"perfect\" score of "<<*it<<endl;}return 0;
}

这个程序传递指向 perfectScore()函数的指针,然后 find_if()算法对每个元素调用这个函数,直到其返回 true为止。下面是这个例子使用 lambda 表达式的版本。这个程序可初步展示 lambda 表达式的威力。不用考虑语法问题,本章后面会详细解释语法。注意这个例子中没有 perfectScore()函数。

auto it = find_if(cbegin(myVector),endIt,[](int i){return i >= 100; })

accumulate()算法

我们经常需要计算容器中所有元素的总和或其他算术值。accumulate()函数就提供了这种功能,该函数在(而非)中定义。通过这个函数的最基本形式可计算指定范围内元素的总和。例如,下面的函数计算 vector 中整数序列的算术平均值。将所有元素的总和除以元素数目,就得到算术平均值。

double arithmeticMean(const vector<int>& nums)
{double sum = accumulate(cbegin(nums),cend(nums),0);return sum / nums.size();
}

accumulate()算法接收的第三个参数是总和的初始值,在这个例子中为 0加法计算的恒等值,表示从 0 开始累加总和。accumulate()的第二种形式允许调用者指定要执行的操作,而不是执行默认的加法操作。这个操作的形式是-元回调。假设需要计算几何平均数。如果一个序列中有 m 个数字,那么几何平均数就是 m 个数字连乘的 m次方根。在这个例子中,调用 accumulate()计算乘积而不是总和。因此这个程序可以这样写:

int product(int num1,int mum2)
{return num1 * num2;
}
double geometricMean(const vector<int>& nums)
{double mult = accumulate(cbegin(nums),cend(nums),1,product);return pow(mult,1.0/nums.size()); //pow() needs <cmath>
}

注意,将 product()函数作为回调传递给 accumulate(),而把累计的初始值设置为 (乘法计算的恒等值)而不是0。下面给出能体现 lambda 表达式威力的第二个例子,geometricMeanLambda()函数可写成以下形式,其中没有使用 product()函数:

double geometricMeanLambda(const vector<int>& nums)
{double mult = accumulate(cbegin(nums),cend(nums),1,[](int num1,int num2){ return num1 * num2;});return pow(mult,1.0 / nums.size());
}

在算法中使用移动语义

与标准库容器一样,标准库算法也做了优化,以便在合适时使用移动语义。这可极大地加速特定的算法,例如 remove()。因此,强烈建议在需要保存到容器中的自定义元素类中实现移动语义。通过实现移动构造函数和移动赋值运算符,任何类都可添加移动语义。它们都被标记为 noexcept,因为它们不应抛出异常。有关如何向自定义类添加移动语义的详细信息,请参阅 9.2.4 节“使用移动语义处理移动”。

std::function

std::function 在头文件中定义,可用来创建指向函数、函数对象或lambda 表达式的类型,从根本上说可以指向任何可调用的对象。它被称为多态函数包装器,可以当成函数指针使用,还可用作实现回调的函数的参数。std::function 模板的模板参数看上去和大多数模板参数都有所不同。语法如下所示:

std::function<R(ArgTypes...)>

R 是函数返回值的类型,ArgTypes 是一个以逗号分隔的函数参数类型的列表。下例演示如何使用 std::function 实现一个函数指针。这段代码创建了一个函数指针 f1,它指向函数 func()。

void func(int num,const string& str)
{cout<<"func("<<num<<", "<<str<<")"<<endl;
}
int main()
{function<void(int,const string&)> f1 = func;f1(1,"test");return 0;
}

当然,上例可使用 auto 关键字,这样就不需要指定 f1 的具体类型了。下面的 f1 定义实现了同样的功能,而且简短得多, 但是编译器推断类型是函数指针(即 void (*f1)(int, const string&))而不是 std::function;

auto f1 = func;

由于 std::function 类型的行为和函数指针一致,因此可传递给标准库算法,如下面这个使用了 find_if()算法的例子所示

bool isEven(int num)
{return num % 2 == 0;
}int main()
{vector<int> vec{1,2,3,4,5,6,7,8,9};function<bool(int)>fcn = isEven;auto result = find_if(cbegin(vec),cend(vec),fcn);if(result == cend(vec)){cout<<"First event number: "<<result<<endl;}else{cout<<"No event number found"<<endl;}return 0;
}

分析以上例子后,你可能感觉 std::function 并不是太有用; 不过 std::function 真正有用的场合是将回调作为类的成员变量。 在接收函数指针作为自定义函数的参数时,也可以使用 std::function。下例定义了 process()函数,这个函数接收一个对 vector 的引用和 std::function。process()函数迭代给定 vector 中的所有元素,然后对每个元素调用指定的函数 f。 参数f可以看成一个回调。print()函数将给定元素打印至控制台。main()函数首先创建一个整数的 vector。接下来调用 process()函数,并传入 print()的函数指针。运行结果是 vector 中的每个元素都被打印出来了。main()函数的最后一部分演示了在 process()函数的 std::function 参数部分能传入 lambda 表达式,这也是std::function 的威力所在。使用普通函数指针无法获得同样的功能。

#include <iostream>
#include <vector>
#include <functional>
using namespace std;
void process(const vector<int>& vec,function<void(int)> f)
{for(auto & i : vec){f(i);}
}
void print(int num)
{cout<<num<<" ";
}int main()
{vector<int> vec{0,1,2,3,4,5,6,7,8,9};process(vec,print);cout<<endl;int sum = 0;process(vec,[&sum](int num){sum += num;});cout<<"sum = "<<sum<<endl;return 0;
}

这个示例的精出如下所未:

xz@xiaqiu:~/study/test/test$ ./test
0 1 2 3 4 5 6 7 8 9
sum = 45
xz@xiaqiu:~/study/test/test$

不使用 std::function 接收回调参数,也可编写如下函数模板:

template<typename T>
void processTemplate(const vector<int>& vec,F f)
{for(auto &i : vec){f(i);}
}

这个函数模板的用法与非模板函数 process()相同, 即 processTemplate()可接收普通函数指针和 lambda 表达式。

lambda 表达式

使用lambda 表达式可编写内堪的匿名函数,而不必编写独立函数或函数对象,使代码更容易阅读和理解。

语法

我们从一个非常简单的 lambda 表达式开始。下面定义一个 lambda 表达式, 它仅把一个字符串写入控制台。lambda 表达式以方括号[]开始(这称为 lambda 引入符),其后是花括号{},其中包含 lambda 表达式体。lambda表达式被赋予自动类型变量 basicLambda。第二行使用普通的函数调用语法执行 lambda 表达式。

auto basicLambda = []{cout<<"Hello from Lambda"<<endl;};
basicLambda();

输出如下所示:

Hello from Lambda

lambda 表达式可以接收参数。参数在圆括号中指定,用逗号分隔开,与普通函数相同。下面是使用参数的

示例:

auto parametersLambda = [](int value){cout<<"The value is "<<value<<endl;};
parametersLambda(42);

如果 lambda 表达式不接收参数,就可指定空圆括号或忽略它们。lambda 表达式可返回值。返回类型在箭头后面指定,称为拖尾返回类型。下例定义的 lambda 表达式接收两个参数,返回它们的和:

auto returningLambda = []{int a,int b}->int{return a+b;};
int sum = returningLambda(11,22);

可以忽略返回类型。如果忽略了返回类型, 编译器就根据函数返回类型推断规则来推断lambda 表达式的返回类型(参见第 1 章)。在上例中,返回类型可以忽略,如下所示:

auto returningLambda = [](int a,int b){ return a + b;};
int sum = returningLambda(11,22);

lambda 表达式可以在其封装的作用域内捕捉变量。例如,下面的 lambda 表达式捕提变量 data,将它用于lambda 表达式体:

double data = 1.23;
auto capturingLambda = [data]{cout<<"Data = "<<data<<endl;};

lambda 表达式的方括号部分称为lambda 捕捉块(capture block)。捕捉变量的意思是可在 lambda 表达式体中使用这个变量。指定空白的捕捉块[]表示不从所在作用域内捕捉变量。如上例所示,在捕捉块中只写出变量名,将按值捕捉该变量。编译器将 lambda 表达式转换为某种未命名的仿函数(即函数对象)。捕捉的变量变成这个仿函数的数据成员。将按值捕捉的变量复制到仿函数的数据成员中。这些数据成员与捕捉的变量具有相同的 const 性质。在前面的capturingLambda 示例中,仿函数得到非 const 数据成员 data,因为捕捉的变量 data 不是 const。但在下例中,仿函数得到 const 数据成员 data,因为捕捉的变量是 const。

const double data = 1.23;
auto capturingLambda = [data]{cout<<"Data = "<<data<<endl;};

仿函数总是实现函数调用运算符 operator()。对于 lambda 表达式, 这个函数调用运算符被默认标记为 const,这表示即使在 lambda 表达式中按值捕捉了非 const 变量,lambda 表达式也不能修改其副本。把 lambda 表达式指定为 mutable,就可以把函数调用运算符标记为非 const:

double data = 1.23;
auto capturingLambda =     [data]()mutable{data*=2;cout<<"Data = "<<data<<endl; };

在这个示例中,非 const 变量 data 是按值捕捉的,因此仿函数得到了一个非 const 数据成员,它是 data 的副本。因为使用了 mutable 关键字,函数调用运算符被标记为非 const,所以 lambda 表达式体可以修改 data 的副本。注意如果指定了 mutable,就必须给参数指定圆括号,即使圆括号为空,也是如此。在变量名前面加上人&,就可按引用捕捉它。下例按引用捕提变量 data,因此 lambda 表达式可以直接在其内部的作用域内修改 data在这个示例中,非 const 变量 data 是按值捕捉的,因此仿函数得到了一个非 const 数据成员,它是 data 的副本。因为使用了 mutable 关键字,函数调用运算符被标记为非 const,所以 lambda 表达式体可以修改 data 的副本。注意如果指定了 mutable,就必须给参数指定圆括号,即使圆括号为空,也是如此。在变量名前面加上人&,就可按引用捕捉它。下例按引用捕提变量 data,因此 lambda 表达式可以直接在其内部的作用域内修改 data

double data = 1.34;
auto capturingLambda = [&data]{ data*=2;};

按引用捕捉变量时,必须确保执行 lambda 表达式时,该引用仍然是有效的。可采用两种方式来捕捉所在作用域内的所有变量。

e [=]: 通过值捕提所有变量。

e [&]: 通过引用捕捉所有变量。

还可以酌情决定捕捉哪些变量以及这些变量的捕提方法,方法是指定一个捕捉列表,其中带有可选的默认捕捉选项。前缀为&的变量通过引用捕捉。不带前绥的变量通过值捕捉。默认捕捉应该是捕捉列表中的第一个元素,可以是=或&。例如

e [&x]: 只通过引用捕捉 x,不捕捉其他变量。

e [x]: 只通过值捕捉 x,不捕提其他变量。

e [=, &x, &y]: 默认通过值捕担,变量 x 和y是例外,这两个变量通过引用捕捉。

e [&,x]: 默认通过引用捕捉,变量 x 是例外,这个变量通过值捕捉。

e [&x, &x]: 非法,因为标识符不允许重复。

e [this]: 捕捉周围的对象。即使没有使用 this->,也可在 lambda 表达式体中访问这个对象。

e [*this]: 捕捉当前对象的副本。如果在执行lambda 表达式时对象不再存在,这将十分有用。

注意:

使用默认捕捉时,只有在 lambda 表达式体中真正使用的变量才会被捕捉,使用值(=)或引用(&)捕捉。未使用的变量不捕捉。

警告:

不建议使用默认捕捉,即使只捕捉在 lambda 表达式体中真正使用的变量,也同样如此。使用=默认捕捉可能在无意中引发昂贵的复制。使用人&默认捕捉可能在无意间修改所在作用域内的变量。建议显式指定要捕捉的变量。

lambda 表达式的完整语法如下所示:

[capture_block](parameters) mutable constexpr
noexcept_specifier attributes
-> return_type {body}

lambda 表达式包含以下部分。

e 捕捉块(capture block): 指定如何捕捉所在作用域内的变量,并供 lambda 主体部分使用。

e 参数(parameter, 可选): lambda 表达式使用的参数列表。 只有在不需要任何参数并且没有指定 mutable、constexpr、noexcep 说明符、属性和返回类型的情况下才能忽略参数列表。该参数列表和普通函数的参数列表类似。

e mutable(可选): 把 lambda 表达式标记为 mutable。

e constexpr(可选): 将 lambda 表达式标记为 constexpr, 从而可在编译时计算。如果满足某些限制条件,即使忽略,也可能为lambda 表达式隐式使用 constexpr。本书不对其进行详细讨论。

e noexcept 说明符(可选),用于指定 noexcept子句,与普通函数的 noexcept 子句类似。

e 特性(attribute,可选), 用干指定lambda表达式的性性,尾性参册第 11 音

e 返回类型(可选): 返回值的类型。如果忽略,编译器会根据函数返回类型推断原则判断返回类型。参见第1章。

泛型lambda 表达式

可以给 lambda 表达式的参数使用自动推断类型功能,而无须显式指定它们的具体类型。要为参数使用自动推断类型功能,只需要将类型指定为auto,类型推断规则与模板参数推断规则相同。下例定义了一个泛型 lambda 表达式 isGreaterThan100。这个 lambda 表达式与 find_if()算法一起使用,一次用于整数 vector,另一次用于双精度 vector:

// Use the generic lambda with a vector of integers.
vector<int> ints{ 11, 55, 101, 200 };
auto it1 = find_if(cbegin(ints), cend(ints), isGreaterThan100);
if (it1 != cend(ints)) {cout << "Found a value > 100: " << *it1 << endl;
}
// Use exactly the same generic lambda with a vector of doubles.
vector<double> doubles{ 11.1, 55.5, 200.2 };
auto it2 = find_if(cbegin(doubles), cend(doubles), isGreaterThan100);
if (it2 != cend(doubles)) {cout << "Found a value > 100: " << *it2 << endl;
}

lambda 捕捉表达式

lambda 捕提表达式允许用任何类型的表达式初始化捕提变量。这可用于在 lambda 表达式中引入根本不在其内部的作用域内捕捉的变量,例如,下面的代码创建一个lambda 表达式,其中有两个变量; myCapture 使用lambda 捕提表达式初始化为字符串“Pi:”,pi 在内部的作用域内按值捕捉。注意,用捕捉初始化器初始化的非引用捕提变量,如 myCapture,是通过复制来构建的,这表示省略了 const 限定符:

double pi = 3.1415;
auto myLambda = [myCapture = "Pi: ", pi]{ cout << myCapture << pi; };

lambda 捕提变量可用任何类型的表达式初始化,也可用 std::move()初始化。这对于不能复制、只能移动的对象而言很重要,例如 unique_ptr。默认情况下,按值捕捉要使用复制语义,所以不可能在 lambda 表达式中按值捕捉 unique_ptr。使用 lambda 捕所表达式,可通过移动来捕捉它,例如:

auto myPtr = std::make_unique<double>(3.1415);
auto myLambda = [p = std::move(myPtr)]{ cout << *p; };

捕提变量允许使用与所在作用域(enclosing scope)内相同的名称。前面的示例可以写为:

auto myPtr = std::make_unique<double>(3.1415);
auto myLambda = [myPtr = std::move(myPtr)]{ cout << *myPtr; };

将lambda 表达式用作返回类型

使用前面讨论的 std::function,可从函数返回 lambda 表达式,分析以下定义:

function<int(void)> multiplyBy2Lambda(int x)
{return [x]{ return 2 * x; };
}

这个函数的主体部分创建一个lambda 表达式,通过值捕捉所在作用域的变量 x,并返回一个整数,这个整数是传给 multiplyBy2Lambda()的值的两倍。multiplyBy2Lambda()函数的返回类型为 function<int(void)>,即一个不接收参数并返回一个整数的函数。函数体中定义的 lambda 表达式正好匹配这个原型。变量 x 通过值捕捉, 因此,在 lambda 表达式从函数返回之前,x 值的副本被绑定至 lambda 表达式中的 x。可按如下方式调用该函数:

function<int(void)> fn = multiplyBy2Lambda(5);
cout << fn() << endl;

通过 auto 关键字可以简化这个调用:

auto fn = multiplyBy2Lambda(5);
cout<<fn()<<endl;

输出为 10。

使用第 1 章介绍的返回类型推导原则,可更简洁地编写 multiplyBy2Lambda()函数,如下所示:

auto multiplyBy2Lambda(int x)
{return [x]{ return 2 * x; };
}

multiplyBy2Lambda()函数通过值捕提了变量 x: [x]。假设这个函数重写为通过引用捕捉变量: [&x]。下面这段代码不能正常工作,因为 lambda 表达式会在程序后面执行,而不会在 multiplyBy2Lambda()函数的作用域内执行,在那里 x 的引用不再有效:

auto multiplyBy2Lambda(int x)
{return [&x]{ return 2*x; } //BUG!
}

将lambda 表达式用作参数

18.2 节“std::function”介绍过,std::function 类型的函数形参可接收 lambda 表达式实参。在本节的示例中,process()函数接收 lambda 表达式作为回调。 本节还解释了 std::function 的替代物, 即函数模板。 processTemplate()函数模板示例也能接收 lambda 表达式实参。

标准库算法示例

本节列举将两个标准库算法(count_if)和 generate()和 lambda 表达式结合使用的示例。 更多示例在本章后面列出。

  1. count_if()

下例通过 count_if()算法计算给定 vector 中满足特定条件的元素个数。通过 lambda 表达式的形式给出条件,这个 lambda 表达式通过值捕提所在作用域内的 value 变量。

vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int value = 3;
int cnt = count_if(cbegin(vec), cend(vec),
[value](int i){ return i > value; });
cout << "Found " << cnt << " values > " << value << endl;

输出如下所示:

Found 6 values > 3

可对上面的这个例子进行扩展,以演示通过引用捕捉变量的方式。下面的 lambda 表达式通过递增所在作用域内按引用捕捉的一个变量,来计算调用次数。

vector<int> vec = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int value = 3;
int cntLambdaCalled = 0;
int cnt = count_if(cbegin(vec), cend(vec),
[value, &cntLambdaCalled](int i){ ++cntLambdaCalled; return i > value; });
cout << "The lambda expression was called " << cntLambdaCalled
<< " times." << endl;
cout << "Found " << cnt << " values > " << value << endl;

输出如下所示:

The lambda expression was called 9 times.
Found 6 values > 3
  1. generate()

generate()算法需要一个迭代器范围, 它把该欠代器范围内的值蔡换为从函数返回的值, 并作为第三个参数。下例结合 generate()算法和一个 lambda 表达式将 2、4、8、16 等值填充到 vector。

vector<int> vec(10);
int value = 1;
generate(begin(vec), end(vec), [&value]{ value *= 2; return value; });
for (const auto& i : vec)
{cout << i << " ";
}

输出如下所示:

2 4 8 16 32 64 128 256 512 1024

画数对象

在类中,可重载函数调用运算符,使类的对象可取代函数指针。将这些对象称为函数对象(function object),或称为仿函数(functor)。很多标准库算法,例如 find_if()以及 accumulate(),可接收函数指针、lambda 表达式和仿函数作为参数,以更改函数行为。C++提供了一些预定义的仿函数类,这些类定义在头文件中,用于执行最常用的回调操作。如果必须创建函数或仿函数类,并指定一个不与其他名称冲突的名称,则使用该名称会带来很大的思维负担, 其实概念非常简单。在此类情况下, 通过 lambda 表达式表示的匿名(未命名)函数可以带来极大便利。lambda表达式的语法更简单,代码也更容易理解。前面讨论了这些内容。不过本节要讨论仿函数,以及如何使用预定义的仿函数类,或许有时会遇到它们。

头文件也可能包含诸如 bindlst()、bind2nd()、mem_fun()、mem_fun_ref()和 ptr_fun()的函数。C++17 标准正式删除了这些函数,因此本书不再讨论它们,你也应当避免使用它们。

注意:

建议尽可能使用 lambda 表达式,而不是小型函数对象,因为 lambda 表达式更便于使用、读取和理解。

算术函数对象

C++提供了 5 类二元算术运算符的仿函数类模板: plus、minus、multiplies、divides 和 modulus。此外提供了一元的取反操作。这些类对操作数的类型模板化,是对实际运算符的包装。它们接收一个或两个模板类型的参数,执行操作并返回结果。下面是一个使用 plus 类模板的示例:

plus<int> myPlus;
int res = myPlus(4, 5);
cout << res << endl;

这个例子的做法显得非常愚蠢,因为可直接使用运算符 operator+,所以没有理由使用 plus 类模板。算术函数对象的好处在于可将它们以回调形式传递给算法,而使用算术运算符时却不能直接这样做。例如,本章之前讨论的 geometricMean()函数的实现使用了 accumulate()函数,并传入一个指向 product()回调的函数指针,用于将两个整数相乘。可利用预定义的 multiplies 函数对象重写它:

double geometricMean(const vector<int>& nums)
{double mult = accumulate(cbegin(nums), cend(nums), 1, multiplies<int>());return pow(mult, 1.0 / nums.size());
}

表达式 multiplies()创建一个新的 multiplies 仿函数类对象,并通过 int 类型实例化。其他算术函数对象的行为是类似的。

警告;

” 算术函数对象只不过是对算术运算符的简单包装。如果在算法中使用函数对象作为回调,务必保证容器中的对象实现了恰当的操作,例如 operator*或 operator+。

透明运算符仿函数

C++支持透明运算符仿函数,允许忽略模板类型参数。例如,可只指定 multiplies<>()而非 multiplies0):

double geometricMeanTransparent(const vector<int>& nums)
{double mult = accumulate(cbegin(nums), cend(nums), 1, multiplies<>());return pow(mult, 1.0 / nums.size());
}

这些透明运算符仿函数的一个重要特性是,它们是异构的,即它们不仅比非透明运算符仿函数更简明,而且具有真正的函数性优势。例如,下面的代码使用透明运算符仿函数和双精度数 1.1 作为初始值,而 vector 包含整数。accumnulate()会把结果计算为 double 值,result 是 6.6:

vector<int> nums{ 1, 2, 3 };
double result = accumulate(cbegin(nums), cend(nums), 1.1, multiplies<>());

如果这些代码使用非透明运算符仿函数, accumulate()会把结果计算为整数, result 就是 6。 编译这些代码时,编译器会给出可能丢失数据的警告:

vector<int> nums{ 1, 2, 3 };
double result = accumulate(cbegin(nums), cend(nums), 1.1, multiplies<int>());

比较函数对象

除算术函数对象类外,C++语言还提供了所有标准的比较: equal_ to、not_equal_to 、less、greater、less_equal和 greater_equal。第 17 章提到,priority_queue 和关联容器使用 less 作为元素的默认比较操作。下面将介绍如何修改这个比较条件。下面的 priority_queue 例子使用默认比较运算符 std::less:

priority_queue<int> myQueue;
myQueue.push(3);
myQueue.push(4);
myQueue.push(2);
myQueue.push(1);
while (!myQueue.empty())
{cout << myQueue.top() << " ";myQueue.pop();
}

这个程序的输出如下所示:

4 3 2 1

从中可看到,根据 less 比较规则,queue 中的元素按降序进行删除。将 greater 指定为比较模板参数,可将这个比较修改为 greater。priority_queue 模板定义如下所示;

template <class T, class Container = vector<T>, class Compare = less<T>>;

遗城的是,Compare 类型参数是最后一个参数,这意味着要指定比较操作,还必须指定容器类型。如果希望 priority_queue 通过 greater 按升序对元素排序,需要把上例中的 priority_queue 定义改为:

priority_queue<int, vector<int>, greater<>> myQueue;

这个程序的输出如下所示:

1 2 3 4

注意,使用透明运算符 greater<>定义了 myQueue。事实上,建议始终使用接收比较回调(comparator)类型的标准库容器的透明运算符。与使用非透明运算符相比,使用透明比较回调的性能稍好一些。例如,如果map使用非透明比较回调,执行查询(将给定的键作为字符串字面量)可能导致创建多余的副本,因为必须从字符串字面量构建 string 实例。使用透明比较回调时,可避免这样的复制。本章后面要学习的几个算法都要求比较回调,届时这些预定义的比较运算符可以提供方便。

逻辑函数对象

C++为 3 个逻辑操作提供了函数对象类,它们分别是: logical_not(operator !)、logical_ and(operator&&)和logical_or(operator ll)。逻辑操作只操作 true 和 false 值。按位函数对象见 18.4.4 节。例如,可使用逻辑仿函数来实现 allTrue()函数,这个函数检查容器中的所有布尔标志是否都为 true:

bool allTrue(const vector<bool>& flags)
{return accumulate(begin(flags), end(flags), true, logical_and<>());
}

类似地,可使用 logical_or 仿函数实现 anyTrue()函数,如果容器中至少有一个布尔标志为 true,那么这个函数返回 true:

bool anyTrue(const vector<bool>& flags)
{return accumulate(begin(flags), end(flags), false, logical_or<>());
}

注意:

allTrue()和 anyTrue()函数只是示例。事实上,标准库提供了 std::all_of()和 any_of()算法,这些算法执行相同的操作,但具有短路计算优势,因此性能更好。

按位函数对象

C++为所有按位操作添加了函数对象,它们分别是: bit_and(operator&)、bit_or(operator)、bit_xor(operator)和 bit_not(operator~)。例如,这些按位仿函数可与 tansform()算法(本章后面描述)结合使用,以便在容器的所有元素上执行按位操作。

函数对象适配器

在使用 C++标准提供的基本函数对象时,往往会有不搭配的感觉。例如,使用 find_if()时,不能通过 less函数对象找到比某个值小的元素,因为 find_if()每次只向回调传递一个参数而不是两个参数。函数适配器(fonction adapter)对象试图解决这个问题和其他问题。这样,就可以适配函数对象、lambda 表达式和函数指针。函数适配器对函数组合(functional composition)提供了一些支持,也就是能将函数组合在一起,以精确提供所需的行为。

  1. 绑定器

绑定器(binder)可用于将函数的参数绑定至特定的值。为此要使用头文件中定义的 std::bind(),它允许采用灵活的方式绑定可调用的参数。既可将函数的参数绑定至固定值,甚至还能重新安排函数参数的顺序。下面通过一个例子进行解释。

假定有一个 func()函数,它接收两个参数,

void func(int num, string_view str)
{cout << "func(" << num << ", " << str << ")" << endl;
}

下面的代码演示如何通过 bind()将 func()函数的第二个参数绑定至固定值 myString。结果保存在f1()中。 使用 auto 关键字是因为 C++标准未指定 bind()的返回类型,因而是特定于实现的。没有绑定至指定值的参数应该标记为_1、 _2 和_3 等。这些都定义在 std::placeholders 名称空间中。在f1()的定义中,_1 指定了调用 func()时,f1()的第一个参数应该出现的位置。之后,就可以用一个整型参数调用f1()。

string myString = "abc";
auto f1 = bind(func, placeholders::_1, myString);
f1(16);

输出如下所示

func(16,abc)

bind()还可用于重新排列参数的顺序,如下列代码所示。_2 指定了调用 func()时,f2()的第二个参数应该出现的位置。换名话说,f2()绑定的意义是: f2()的第一个参数将成为函数 func()的第二个参数,f2()的第二个参数将成为函数 func的第一个参数。

auto f2 = bind(func, placeholders::_2, placeholders::_1);
f2("Test", 32);

输出如下所示:

func(32, Test)

如第 17 章所述,头文件定义了 std::ref()和 cref()辅助模板函数。它们可分别用于绑定引用或const引用。例如,假设有以下函数:

void increment(int& value) { ++value; }

如果调用了这个函数,如下所示,index 的值就是 1:

int index = 0;
increment(index);

如果使用 bind()调用它,如下所示,index 的值就不递增,因为建立了 index 的一个副本,并把这个副本的引用绑定到 increment()函数的第一个参数:

int index = 0;
auto incr = bind(increment, index);
incr();

使用 std::ref()正确传递对应的引用后会递增 index:

int index = 0;
auto incr = bind(increment, ref(index));
incr();

结合重载函数使用时,绑定参数会出现一个小问题。假设有下面两个名为 overloaded()的重载函数。一个接收整数参数,另一个接收浮点数参数,

void overloaded(int num) {}
void overloaded(float f) {}

如果要对这些重载的函数使用 bind(),那么必须显式地指定绑定这两个重载中的哪一个。下面的代码无法成功编译

auto f3 = bind(overloaded, placeholders::_1); // ERROR

如果需要绑定接收浮点数参数的重载数的参数,需要使用以下语法:

auto f4 = bind((void(*)(float))overloaded, placeholders::_1); // OK

下面列举另一个使用 bind()的例子: 通过 find_if()算法找出序列中第一个大于或等于 100 的元素。本章前面解决这个问题时,把指向 perfectScore()函数的指针传递给 find_if()。可以使用比较仿函数 greater_equal 和 bind()重写该例。下面的代码使用 bind()把 greater_equal 的第二个参数绑定到固定值 100:

// Code for inputting scores into the vector omitted, similar as earlier.
auto endIter = end(myVector);
auto it = find_if(begin(myVector), endIter,
bind(greater_equal<>(), placeholders::_1, 100));
if (it == endIter)
{cout << "No perfect scores" << endl;
}
else
{cout << "Found a \"perfect\" score of " << *it << endl;
}

当然,在这里建议使用 lambda 表达式:

auto it = find_if(begin(myVector), endIter, [](int i){ return i >= 100; });

警告:

在 C++11之前有bind2nd()和bindlst(),在 C++11 中已不建议使用它们,在 C++17 标准中,已将它们完全删除。请改用 lambda 表达式和 bind()。

  1. 取反器

    not_fn

取反器(Negators)类似于绑定器(binder),但对调用结果取反。例如,如果想要找到测试分数序列中第一个小于 100 的元素,那么可以对 perfectScore()的结果应用 not1()取反器适配器,如下所示:

// Code for inputting scores into the vector omitted, similar as earlier.
auto endIter = end(myVector);
auto it = find_if(begin(myVector), endIter, not_fn(perfectScore));
if (it == endIter)
{cout << "All perfect scores" << endl;
}
else
{cout << "Found a \"less-than-perfect\" score of " << *it << endl;
}

not_fn()仿函数对作为参数传入的每个调用结果取反。注意在这个示例中,也可以使用 find_if_not()算法。从以上讨论可以看出,仿函数和适配器的使用很快就会变得非常复杂。建议尽量使用lambda 表达式而不是仿函数。例如,前面使用not_func()取反器的 find_if()调用可以用 lambda 表达式更简洁地表达:

auto it = find_if(begin(myVector), endIter, [](int i){ return i < 100; });

not1 和 not2

C++17 引入了 std::not_fn()适配器。 在 C++17 之前,可使用 std::not1()和 not2()适配器。notl 中的“1” 是指:它的操作数必须是一个一元函数(只接收一个参数)。如果操作数是二元函数(接收两个参数), 则必须改用 not2()。下面是一个示例,

// Code for inputting scores into the vector omitted, similar as earlier.
auto endIter = end(myVector);
function<bool(int)> f = perfectScore;
auto it = find_if(begin(myVector), endIter, not1(f));

如果想将 not1()用于自己的仿函数类,则必须确保仿函数类的定义包含两个 typedef: argument type 和result_ type 。如果想要使用 not2(),则仿函数类的定义必须提供 3 个 typedef: first_argument_type、second_ argument_type 和 result_type。为此,最简便的方式是从 unary_function 或 binary_function 派生自己的函数对象类,具体取决于使用的是一个参数还是两个参数。在中定义的两个类在所提供函数的参数和返回类型上模板化。例如

class PerfectScore : public std::unary_function<int, bool>
{public:result_type operator()(const argument_type& score) const{return score >= 100;}
};

可采用如下方式使用这个仿函数:

auto it = find_if(begin(myVector), endIter, not1(PerfectScore()));

注意;

C++17 标准中已不园成使用not1()和not2().从 C++11 开始,已不赞成使用unary_function 和binary function;到了 C++17,则正式删除。在新代码中,尽量避免使用这些函数。

调用成员函数

假设有一个对象容器,有时需要传递一个指向类方法的指针作为算法的回调。例如,假设要对序列中的每个字符串调用 empty()方法,找到 string vector 中的第一个空字符串。然而,如果将指向 string::empty()的指针传递给 find_if(),这个算法无法知道接收的是指向方法的指针,而不是普通函数指针或仿函数。调用方法指针的代码和调用普通函数指针的代码是不一样的,因为前者必须在对象的上下文中调用。C++提供了 mem_fn()转换函数,在传递给算法之前可以对函数指针调用这个函数。下面的例子演示了调用方式。注意,必须将方法指针指定为&string::empty。&string::部分不是可选的。

void findEmptyString(const vector<string>& string)
{auto endIter = end(strings);auto it = find_if(begin(strings),endIter,mem_fn(&string::empty));if(it == endIter){cout<<"No empty strings!"<<endl;}else{cout<<"Empty string at position: "<<static_cast<int>(it - begin(strings))<<endl;}
}

mem_fn()生成一个用作 find_if()回调的函数对象。每次调用它时,都会对参数调用 empty()方法。即使容器内保存的不是对象本身,而是对象指针,mem_fn()的使用方法也完全一样,例如:

void findEmptyString(const vector<string*>& strings)
{auto endIter = end(strings);auto it = find_if(begin(strings),endIter,mem_fn(&string::empty)); Remainder of function omitted because it is the same as earlier
}

mem_fn()并非实现 findEmptyString()函数的最简单方式。使用 lambda 表达式,可以用可读性更强、更优雅的方式实现这个函数。下面是通过 lambda 表达式实现的用于对象容器的代码;

void findEmptyString(const vector<string>& strings)
{auto endIter = end(strings);auto it = find_if(begin(stings),endIter,[](const string& str){return str.empty();})// Remainder of function omitted because it is the same as earlier
}

类似地,下面使用 lambda 表达式的实现用于对象指针的容器;

void findEmptyString(const vector<string*>& strings)
{auto endIter = end(strings);auto it = find_if(begin(strings),endIter,[](const string* str){return str->empty();});
}

std::invoke()

C++17 引入了 std::invoke(),std::invoke()在中定义,可用于通过一组参数调用任何可调用对象。下例使用了三次 invoke(): 一次调用普通函数,另一次调用 lambda 表达式,还有一次调用 string 实例的成员

void printMessage(string_view message){cout<<message<<endl;}
int main()
{invoke(printMessage,"Hello invoke");invoke([](const auto& msg){cout<<msg<<endl;},"Hello invoke");string msg = "Hello invoke.";cout<<invoke(&string::size,msg)<<endl;
}

invoke()本身的作用并不大,因为可以直接调用函数或 lambda 表达式。但在模板代码中,如果需要调用任意可调用对象,invoke()的作用会发挥出来。

编写自己的函数对象

如果需要完成不适合用 lambda 表达式执行的更复杂任务,那么可编写自己的函数对象,以执行预定义仿函数不能执行的更特定任务。下面是一个简单的函数对象示例:

class myIsDigit
{public:bool operator()(char c)const { return ::isdigit(c) != 0;}
};bool isNumber(string_view str)
{auto endIter = end(str);auto it = find_if(begin(str),endIter,not_fn(myIsDigit()));return (it == endIter);
}

注意,只有当 myIsDigit 类的重载函数调用运算符是 const 时,才能将其对象传递给 find_if()。

警告:

算法可生成给定谓词(如仿函数和lambda 表达式)的多份副本,并对不同的元素调用不同的副本。 这对此类谓词的副作用施加了严重限制。对于仿函数,函数调用运算符必须为 const。 因此,编写仿函数时,不能让仿函数依赖在调用之间保持一致的对象的任何内部状态。lambda表达式与此类似,不能将其标记为mutable。也有一此例外,例如,speneraten和 senerate nf本接收在状太的谓词,但这样也会生成谓词的一个副本,它们不返回那个副本,因此在算法完成后无权访问对状态的更改。唯一的例外是 for_each()。它将给定谓词复制到for_each()算法一次,在完成时返回副本。可通过这个返回值来访问更改后的状态。如果其他算法需要有状态的谓语,可将谓词包装在 std::reference_wrapper(可使用 std::ref()来创建)中。在 C++11 之前,在函数作用域内局部定义的类不能用作模板参数。这个限制已经取消了。下面的例子演了这一点:

在 C++11 之前,在函数作用域内局部定义的类不能用作模板参数。这个限制已经取消了。下面的例子演示了这一点

bool isNumber(string_view str)
{class myIsDigit{public:bool operator()(char c) const { return ::isdigit(c) != 0;}};auto endIter = end(str);auto it = find_if(begin(str),endIter,not_fn(myIsDigit()));return (it == endIter);
}

注意;

从这些例子可以看出,利用 lambda 表达式可以编写更便于阅读、更整洁的代码。建议使用简单的 lambda从这些例子可以看出,利用 lambda 表达式可以编写更便于阅读、更整洁的代码。建议使用简单的 lambda

算法详解

第 16 章列出了所有可用的标准库算法,并分为不同的类别。大多数算法在头文件中定义,但有几个算法位于和中。它们都在 std 名称空间中。本章不讨论所有可用的算法,只选择几个类别,并举例说明。知道如何使用它们后,就能顺利地使用其他算法了。标准库参考资源包含所有算法的总结,

迁代器

首先讨论迭代器。迭代器有 5 类: 输入、输出、正向、双向和随机访问迭代器。第 17 章描述了这些迭代器。这些迭代器没有正式的类层次关系,因为每个容器的实现都不是标准层次关系中的一部分。不过,根据这些迭代器要实现的功能,可以推导出其层次关系。确切地讲,每个随机访问迭代器也是双向的,每个双向迭代器也是正向的,每个正向迭代器也是输入迭代器。满足输出迭代器要求的所代器称为可变迭代器(mutable iterator),和否则称为不可变迁代器(constant iterator) 。图 18-1 呈现了这个层次关系。 图 18-1 中使用虚线表示这张图中展示的并非真正的类算法指定要使用的迭代器类型的标准方法是,在迭代器模板类型参数中使用以下名称: Inputlterator、Outputlterator、Forwardlterator、Bidirectionallterator 和 RandomAccessIterator。这些只是名称,没有提供绑定类型的检查。因此,可在调用需要 RandomAccessIterator 的算法时,传入双向选代器。模板不会进行类型检查,因此允许这样的实例化。然而,函数中使用随机访问迭代器功能的代码,在使用双向迭代器时将无法成功编译。因此,这个需求是强制的,只不过不是在期待的地方实施。错误消息可能让人感到有点迷惑。例如,泛型算法 sort()需要随机访问迭代器,而 list 只提供了一个双向迭代器,因此如果在Visual C++ 2017 中试图对 list 应用这个算法, 将会出现十分难以理解的错误, 错误消息共 30 行, 部分代码如下:

...\vc\tools\msvc\14.11.25301\include\algorithm(3032): error C2784: 'unknown-type
std::operator -(const std::move_iterator<_RanIt> &,const std::move_iterator<_
RanIt2> &)': could not deduce template argument for 'const std::move_iterator<_
RanIt> &' from 'std::_List_unchecked_iterator<std::_List_val<std::_List_simple_
types<int>>>'
...\vc\tools\msvc\14.11.25301\include\xutility(2191): note: see declaration of
'std::operator -'

非修改序列算法

非修改序列算法包括在某个范围内搜索元素的函数、比较两个范围的函数以及许多工具算法。

搜索算法

前面介绍了使用两个搜索算法(find()和 find_if())的示例。标准库提供了基本 find()算法的一些其他变种,些算法对元素序列执行操作。16.2.18 节中的“二又树搜索算法”部分描述了可供使用的不同搜索算法,还包了算法的复杂度。

所有算法都使用默认的比较运算符 operator==和 operator<,还提供了重载版本,以允许指定比较回调下面是一些搜索算法的示例

//The list of elements to be searched
vector<int> myVector = {5, 6, 9, 8, 8, 3};
auto beginIter = cbegin(myVector);
auto endIter = cend(myVector);//Find the first element that does not satisfy the given lambda expression
auto it = find_if_not(beginIter,endIter,[](int i){ return i < 8;});
if(it != endIter)
{cout<<"First element not < 8 is "<<*it<<endl;
}
// Find the first pair of matching consecutive elements
it = adjacent_find(beginIter,endIter);
if(it != endIter)
{cout<<"Found two consecutive equal elements with value " << *it << endl;"
}
// Find the first of two values
vector<int> targets = { 8, 9 };
it = find_first_of(beginIter, endIter, cbegin(targets), cend(targets));
if (it != endIter)
{cout << "Found one of 8 or 9: " << *it << endl;
}
// Find the first subsequence
vector<int> sub = { 8, 3 };
it = search(beginIter, endIter, cbegin(sub), cend(sub));
if (it != endIter)
{cout << "Found subsequence {8,3}" << endl;
} else
{cout << "Unable to find subsequence {8,3}" << endl;
}
// Find the last subsequence (which is the same as the first in this example)
auto it2 = find_end(beginIter,endIter, cbegin(sub), cend(sub));
if(it != it2)
{cout<<"Error: search and find end found different subsequences "<<"event through there is only one match "<<endl
}
// Find the first subsequence of two consecutive 8s
it = search_n(beginIter,endIter,2,8);
if(it != endIter)
{cout<<"Found two consecutive 8s "<<endl;
}
else
{cout<<"Unable to find two consecutive 8s "<<endl;
}

输出

First element not < 8 is 9
Found two consecutive equal elements with value 8
Found one of 8 or 9: 9
Found subsequence {8,3}
Found two consecutive 8s

专用的搜索算法

C++17 给 search()算法增加了额外的可选参数,人允许指定要使用的搜索算法。有三个选项: default_searcher、boyer_moore_searcher 或 boyer_moore_horspool_searcher,它们都在中定义。后两个选项实现了知名的 Boyer-Moore 和 Boyer-Moore-Horspool 搜索算法。它们十分高效,可用于在一大块文本中查找子字符串。Boyer-Moore 搜索算法的复杂度如下,X是在其中搜索的序列(haystack)的大小,M 是要查找的模式(needle)的大小。

如果未找到模式,最坏情况下的复杂度为 O(N + M)

如果找到模式,最坏情况下的复杂度为 O(NM)

这些是理论上最坏情况下的复杂度。在实际中,这些专用搜索算法比 O(N)更好,比默认算法更快! 因为它们可以跳过字符,而非查找 haystack 中的每个字符。它们还有一个有趣的特性,即 needle 越长,速度越快,因为那时可跳过 haystack 中的更多字符。Boyer-Moore 和 Boyer-Moore-Horspool 算法的区别在于,在初始化以及算法的每个循环迭代中,后者的固定开销较少,但是,后者在最坏情况下的复杂度明显高于前者算法。因此,需要根据具体情况进行选择。下面是一个使用 Boyer-Moore 搜索算法的例子,

string text = "This is the haystack to search a needle in.";
string toSearchFor = "needle";
auto searcher = std::boyer_moore_searcher(
cbegin(toSearchFor), cend(toSearchFor));
auto result = search(cbegin(text), cend(text), searcher);
if (result != cend(text))
{cout << "Found the needle." << endl;
}
else
{cout << "Needle not found." << endl;
}

比较算法

可通过 3 种不同的方法比较整个范围内的元素: equal()、mismatch()和 lexicographical_compare()。这些算法的好处是可比较不同容器内的范围。例如,可比较 vector 和 list 的内容。一般情况下,这些算法最适用于顺序容器。这些算法的工作方法是比较两个集合中对应位置的值。下面列出每个算法的工作方式。

e equal(0): 如果所有对应元素都相等,则返回 true。最初,equal()接收三个迭代器,分别是第一个范围的首尾迭代器,以及第二个范围的首迭代器。该版本要求两个范围的元素数目相同。从 C++14 开始,有了接收 4 个迭代器的重载版本,分别是第一个范围的首尾迭代器,以及第二个范围的首尾迭代器。该版本可处理不同大小的范围,为保险起见,建议始终使用四迭代器版本。

e mismatch(): 返回多个迭代器,每个范围对应一个迭代器,表示范围内不匹配的对应元素。与 equal()一样,存在三迭代器版本和四迭代器版本。同样,为保险起见,建议使用四友代器版本。elexicographical_compare(): 如果第一个范围内的第一个不相等元素小于第二个范围内的对应元素,或如果第一个范围内的元素个数少于第二个范围,且第一个范围内的所有元素都等于第二个范围内对应的初始子序列,那么返回 true。名称 lexicographical_compare 来自这个算法对字符串比较规则的模仿,但对规则集进行了扩展,能够处理任意类型的对象。

注意:

如果要比较两个同类型容器的元素,可使用运算符 operator–和 operator<,而不是 equal0和 lexicographical compare()。这些算法可用于比较不同容器类型的元素序列、子范围和 C 风格数组等。

下面是使用这些算法的示例:

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <list>
using namespace std;
//Function template to populate a container of ints
//The container must support push_back()
template<typename Container>
void populateContainer(Container& cont)
{int num;while(true){cout<<"Enter a number (0 to quit):";cin>>num;if(num == 0){break;}cont.push_back(num);}
}
int main()
{vector<int> myVector;list<int> myList;cout<<"Populate the vector: "<<endl;populateContainer(myVector);cout<<"Populate the list:"<<endl;populateContainer(myList);//Compare the two containersif(equal(cbegin(myVector),cend(myVector),cbegin(myList),cend(myList))){cout<<"The two container have equal elements"<<endl;}else{//If the containers were not equal, find out why not auto miss = mismatch(cbegin(myVector),cend(myVector),cbegin(myList),cend(myList));cout<<"The following initial elements are the same in "<<"the vector and the list:"<<endl;for(auto i = cbegin(myVector);i != miss.first;++i){cout<<*i<<'\t';}cout<<endl;}//now order themif(lexicographical_compare(cbegin(myVector),cend(myVector),cbegin(myList),cend(myList))){cout<<"The vector is lexicographically first "<<endl;}else{cout<<"The list is lexicographically first "<<endl;}return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Populate the vector:
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):0
Populate the list:
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):1
Enter a number (0 to quit):0
The following initial elements are the same in the vector and the list:
1   2   1
The list is lexicographically first

计数算法

非修改计数算法有 all_of()、any_of()、none_of()、count()和 count_if()。下面是前 3 个算法的示例。count_if()的示例在本章前面。

//all_of()
vector<int> vec2 =  {1,1,1,1}
if(all_of(cbegin(vec2),cend(vec2),[](int i){return i == 1;}))
{cout<<"All elements are == 1"<<endl;
}
else
{cout<<"Not all elements are == 1"<<endl;
}//any_of()
vector<int> vec3 = {0,0,1,0};
if(any_if(cbegin(vec3),cend(vec3),[](int i){return i == 1;}))
{cout<<"At least one element == 1"<<endl;
}
else
{cout<<"No elements are == 1"<<endl;
}
//none of()
vector<int> vec4 = {0,0,0,0};
if(none_of(cbegin(vec4),cend(vec4),[](int i){ return i == 1;}))
{cout<<"All elements are != 1"<<endl;
}
else
{cout<<"Some elements are == 1"<<endl;
}

输出

All elements are == 1
At least one element == 1
All elements are != 1

修改序列算法

标准库提供了多种修改序列算法,这些算法执行的任务包括: 从一个范围向另一个范围复制元素、删除元素以及反转某个范围内元素的顺序。一些修改算法涉及源范围和目标范围的概念。从源范围读取元素,然后在目标范围中进行修改。其他算法就地(in place)执行操作;也就是说,只需要一个范围。

警告

修改算法不能将元素插入目标范围中,仅可重写/修改目标范围中已经存在的元素。第 21 章将描述如何使用迁代器适配器,在目标范围中真正插入元素。

注意:

”map 和 multimap 的范围不能用作修改算法的目标范围。这些算法改写全部元素,而在 map 中,元素是键值对。map 和 multimap 将键标记为 const,因此不能为其赋值。set 和 multiset 也是如此。替换方案是使用插入迭代器,详见第 21 章。

16.2.18 节的“修改序列算法”部分列出了所有可用的修改算法,并给出了描述信息。本节列举其中不少算法的代码示例。如果理解了本节讲述的算法,就应能使用没有给出示例的其他算法。

  1. 转换

transform()算法对范围内的每个元素应用回调,期望回调生成一个新元素,并保存在指定的目标范围中。如果希望 ransform()将范围内的每个元素蔡换为调用回调产生的结果, 那么源范围和目标范围可以是同一范围。其参数是源序列的首尾途代器、目标序列的首和迭代器以及回调。例如,可按如下方式将 vector 中的每个元素增加 100:

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <list>
using namespace std;template<typename Container>
void populateContainer(Container& cont)
{int num;while(true){cout<<"Enter a number (0 to quit):";cin>>num;if(num == 0){break;}cont.push_back(num);}
}int main()
{vector<int> myVector;populateContainer(myVector);cout<<"The vector contains: "<<endl;for(const auto& i : myVector){cout<<i<<" ";}cout<<endl;transform(begin(myVector),end(myVector),begin(myVector),[](int i){return i + 100;});cout<<"The vector contains:"<<endl;for(const auto& i : myVector){ cout<<i<<" ";}return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):2
Enter a number (0 to quit):1
Enter a number (0 to quit):21
Enter a number (0 to quit):0
The vector contains:
1 2 3 4 2 1 21
The vector contains:
101 102 103 104 102 101 121

transform()的另一种形式对范围内的元素对调用二元函数,需要将第一个范围的首尾途代器、第二个范围的首迭代器以及目标范围的首和迭代器作为参数。下例创建两个 vector,然后通过 transformO计算元素对的和,并将结果保存回第一个 vector:

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <list>
using namespace std;template<typename Container>
void populateContainer(Container &cont)
{int num;while (true){cout << "Enter a number (0 to quit):";cin >> num;if (num == 0){break;}cont.push_back(num);}
}int main()
{vector<int> vec1, vec2;cout << "Vector1:" << endl; populateContainer(vec1);cout << "Vector2:" << endl; populateContainer(vec2);if (vec2.size() < vec1.size()){cout << "Vector2 should be at least the same size as vector1 " << endl;return 1;}//Create a lambda to print the contents of a containerauto printContainer = [](const auto & container){for (auto &i : container) {cout << i << " ";}cout << endl;};cout << "Vector1: "; printContainer(vec1);cout << "Vector2: "; printContainer(vec2);transform(begin(vec1), end(vec1), begin(vec2), begin(vec1), [](int a, int b) { return a + b;});cout << "Vector1: "; printContainer(vec1);cout << "Vector2: "; printContainer(vec2);    return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Vector1:
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):0
Vector2:
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):54
Enter a number (0 to quit):1
Enter a number (0 to quit):0
Vector1: 1 2 3 4
Vector2: 2 3 54 1
Vector1: 3 5 57 5
Vector2: 2 3 54 1

注意

transform()和其他修改算法通常返回一个引用目标范围内最后一个值后面那个位置(past-the-end)的迭代器。本书中的例子通常都忽略了返回值。

  1. 复制

copy()算法可将一个范围内的元素复制到另一个范围,从这个范围内的第一个元素开始直到最后一个元素。源范围和目标范围必须不同,但在一定限制条件下可以重叠。限制条件如下: 对于 copy(b,e,d),如果d 在b之前,则可以重叠, 但如果 d 处于[b,e]范围,则行为不确定。与所有修改壬法类似,copy()不会向目标范围插入元素,只改写已有的元素。第 21 章将描述如何使用迭代器适配器,使用 copy()向容器或流插入元素。下面举一个使用 copy()的简单例子,这个例子对 vector 应用 resize()方法,以确保目标容器中有足够空间。这个例子将 vecl 中的所有元素复制到 vec2:

vector<int> vec1,vec2;
populateContainer(vec1);
vec2.resize(size(vec1));
copy(cbegin(vec1),cend(vec1),begin(vec2));
for(const auto& i : vec2){ cout<<i<<" ";}

还有一个 copy_backward()算法,这个算法将源范围内的元素反向复制到目标范围。换名话说,这个算法从源范围的最后一个元素开始,将这个元素放在目标范围的最后一个位置,然后在每一次复制之后反向移动。分析copy_backward(),源范围和目标范围必须是不同的,但在一定限制条件下可以重登。限制条件如下: 对于copy_backward(b,e,d),如果 d 在e之后,则能正确重叠,但如果 d 处于(b,e]范围,则行为不确定。前面的例子可按如下代码修改为使用 copy_backward()而不是 copy()。注意第三个参数应该指定 end(vec2)而不是 begin(vec2):

copy_backward(cbegin(vec1),cend(vec1),end(vec2));

得到的输出完全一致。

在使用 copy_if()算法时,需要提供由两个迭代器指定的输入范围、由一个迭代器指定的输出范围以及一个谓词(函数或 lambda 表达式)。该算法将满足给定谓词的所有元素复制到目标范围。记住,复制不会创建或扩大容器,只是替换现有元素。因此,目标范围应当足够大,从而保存要复制的所有元素。当然,复制元素后,最好删除超出最后一个元素复制位置的空间。为便于达到这个目的,copy_if()返回了目标范围内最后一个复制的元素后面那个位置(one-past-the-last-copied element)的迭代器,以便确定需要从目标容器中删除的元素个数。下例演示了这个操作,这个例子只把偶数复制到 vec2:,

vector<int>vec1,vec2;
populateContainer(vec1);
vec2.resize(size(vec1));
auto endIterator = copy_if(cbegin(vec1),cend(vec1),begin(vec2),[](int i){ return i % 2 == 0;});
vec2.erase(endIterator,end(vec2));
for(const auto& i : vec2){ cout<<i<<" ";}

copy_n()从源范围复制n 个元素到目标范围。copy_n()的第一个参数是起始迭代器,第二个参数是指定要复制的元素个数,第三个参数是目标进代器。copy_n()算法不执行任何边界检查,因此一定要确保起始叠代器递增n 个要复制的元素后,不会超过集合的 end(),和否则程序会产生未定义的行为。下面是一个例子:

vector<int> vec1,vec2;
populateContainer(vec1);
size_t cnt = 0;
cout<<"Enter number of elements you want to copy:";
cin>>cnt;
cnt = min(cnt,size(vec1));
vec2.resize(cnt);
copy_n(cbegin(vec1),cnt,begin(vec2));
for(const auto& i : vec2){ cout<<i<<" ";}
  1. 移动

有两个和移动相关的算法: move()和 move_backward()。它们都使用了第 9 章讨论的移动语义。如果要在自定义类型元素的容器中使用这两个算法,那么需要在元素类中提供移动赋值运算符,请参阅下例。main()函数创建了一个带有 3 个 MyClass 对象的 vector,然后将这些元素从 vecSrc 移到 vecDst。 注意这段代码包含两种不同的 move()用法。一种是,move()函数接收一个参数,将 lvalue 转换为 rvalue,在中定义;而另一种是,接收 3 个参数的 move()是标准库的 move()算法,这个算法在容器之间移动元素。有关移动赋值运算符的实现和 std::move()单参数版本的使用,请参阅第9 章。

#include <vector>
#include <iostream>
using namespace std;
class MyClass
{public:MyClass() = default;MyClass(const MyClass& src) = default;MyClass(string_view str):mStr(str){}virtual ~MyClass() = default;//Move assignment operatorMyClass& operator=(MyClass&& rhs) noexcept{if(this == &rhs)return *this;mStr = std::move(rhs.mStr);cout<<"Move operator=(mStr = " << mStr <<")"<<endl;return *this;}void setString(string_view str){mStr = str;}string_view getString() const { return mStr;}private:string mStr;
};
int main()
{vector<MyClass>vecSrc{MyClass("a"),MyClass("b"),MyClass("c")};vector<MyClass>vecDst(vecSrc.size());move(begin(vecSrc),end(vecSrc),begin(vecDst));for(const auto& c : vecDst){ cout<<c.getString()<<" ";}std::cout<<std::endl;return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Move operator=(mStr = a)
Move operator=(mStr = b)
Move operator=(mStr = c)
a b c
xz@xiaqiu:~/study/test/test$

注意:

第 9章解释过,在移动操作中,源对象将处于有效但不确定的状态。在前面的例子中,这意味着在执行移动操作后,不应该再使用 vecSrc 中的元素了,除非使它们重回确定状态; 例如,在它们之上调用方法(不包含任何预置条件),如 setString()。

move_backward()使用和move()同样的移动机制, 但按从最后一个元素向第一个元素的顺序移动对于move()和 move_ backward(),在符合某些限制条件的情况下允许源范围和目标范围重登。限制条件与 copy()和copy_backward()的相同 。

  1. 替换

replace()和 replace_if()算法将一个范围内匹配某个值或满足某个谓词的元素替换为新的值。比如 replace_if()算法的第一个和第二个参数指定了容器中元素的范围。第三个参数是一个返回 true 或 false 的函数或 lambda表达式,如果它返回 true,那么容器中的对应值被替换为第四个参数指定的值,如果它返回 false,则保留原始值。

例如,假定要将容器中的所有奇数值蔡换为 0:

vector<int> vec;
populateContainer(vec);
replace_if(begin(vec),end(vec),[](int i){ return i % 2 != 0;},0);
for(const auto& i : vec){cout<<i<<" ";}

replace()和 replace_if()也有名为 replace_copy()和 replace_copy_if()的变体,这些变体将结果复制到不同的目标范围。它们类似于 copy(),因为目标范围必须足够大,以容纳新元素。

  1. 删除

假设要将某个范围内满足某特定条件的元素删除。你可能想到的第一个解决方案是查看文档,确定容器是否有 erase()方法,然后迭代所有元素,并对每个满足条件的元素调用 erase()。vector 是包含 erase()方法的容器之一。然而,如果对 vector 容器应用 erase(),这个解决方案的效率非常低下,因为要保持 vector 在内存中的连续性,会涉及很多内存操作,因而得到二次(平方)复杂度,所谓二次复杂度,是指运行时间是输入大小的平方的函数,即 O(n^2)。这个解决方案还容易产生错误,因为必须非常小心地确保每次调用 erase()之后迭代器依然有效。例如,如下函数从 string vector 中删除空字符串,而未使用算法。注意,需要在 for 循环中精心操纵 iter:

void removeEmptyStringsWithoutAlgorithms(vector<string>& strings)
{for(auto iter = begin(strings);iter != end(strings);){if(iter->empty())iter = strings.erase(iter);else++iter;}
}

上面的解决方案效率低下,不建议使用。这个问题的正确解决方案是“删除-擦除法”(remove-erase-idiom),下面讲解这种线性时间方法。算法只能访问迭代器抽象,不能访问容器。因此删除算法不能真正地从底层容器中删除元素,而是将匹配给定值或谓词的元素蔡换为下一个不匹配给定值或谓词的元素。为此使用移动赋值。结果是将范围分为两个集合: 一个用于保存要保留的元素,另一个用于保存要删除的元素。返回的迭代器指向要删除的元素范围内的第一个元素。如果真的需要从容器中删除这些元素,必须先使用 remove()算法,然后调用容器的 erase()方法,将从返回的迭代器开始到范围尾部的所有元素删除。这就是删除-擦除法。下面这个示例函数将 string vector 中的空字符串删除:

void removeEmptyStrings(vector<string>& strings)
{auto it = remove_if(begin(strings),end(strings),[](const string& str){ return str.empty();});//Erase the removed elements;strings.erase(it,end(strings));
}int main()
{vector<string> myVector = {"", "one", "", "two", "three", "four"};for (auto& str : myVector) { cout << "\"" << str << "\" "; }cout << endl;removeEmptyStrings(myVector);for (auto& str : myVector) { cout << "\"" << str << "\" "; }cout << endl;return 0;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
"" "one" "" "two" "three" "four"
"one" "two" "three" "four"
xz@xiaqiu:~/study/test/test$

警告;

使用“删除-擦除法”时,切勿忘记 erase()的第二个参数! 如果忘掉第二个参数,erase()将仅从容器中删除一个元素,即作为第一个参数传递的迭代器指向的元素。” remove()和 remove_if()的 remove_copy()和 remove_copy_if()变体不会改变源范围, 而将所有未删除的元素复制到另一个目标范围。这些算法和 copy()类似,要求目标范围必须足够大,以便保存新元素。

注意:

remove()函数系列是稳定的,因为这些函数保持了容器中剩余元素的顺序,尽管这些算法将保留的元素向前移动了。

唯一化

unique()算法是特殊的 remove(),remove()能将所有重复的连续元素删除。list 容器提供了自己的具有同样语义的unique()方法。通党情况下应该对在序序列使用 unique(),介unique也能用于无序序列。unique()的基本形式是就地操作数据, 还有一个名为 unique_copy()的版本,这个版本将结果复制到一个新的目标范围。17.2.4 节的“list 示例: 确定注册情况”部分展示了 list:unique()算法的一个示例,因此这里略去这个算法一般形式的例子。

抽样

sample()算法从给定的源范围返回n 个随机选择的元素,并存储在目标范围。它需要 5 个参数:

  1. 要从中抽样的范围的首尾迭代器。

  2. 目标范围的首迭代器,将随机选择的元素存储在目标范围

  3. 要选择的元素数量

  4. 随机数生成引擎

有关如何使用随机数生成引警的详情,以及如何播下“种子”,请参阅第 20 章。下面是一个示例:

vector<int> vec{ 1,2,3,4,5,6,7,8,9,10 };
const size_t numberOfSamples = 5;
vector<int> samples(numberOfSamples);random_device seeder;
const auto seed = seeder.entropy()?seeder():time(nullptr);
default_random_engine_engine(static_cast<default_random_engine::result_type>(seed));
for(int i = 0;i < 6;++i)
{sample(cbegin(vec),cend(vec),begin(samples),numberOfSamples,engine);for(const auto& sample : samples){cout<<Sample<<" ";}cout<<endl;
}

反转

reverse()算法反转某个范围内元素的顺序。将范围内的第一个元素和最后一个元素交换,将第二个元素和倒数第二个元素交换,依此类推。reverse()最基本的形式是就地运行,要求两个参数: 范围的首尾迭代器。还有一个名为 reverse_copy()的版本,这个版本将结果复制到新的目标范围,它需要 3 个参数: 源范围的首尾迭代器以及目标范围的起始迭代器。目标范围必须足够大,以便保存新元素。

  1. 乱序

shufle()以随机顺序重新安排某个范围内的元素, 其复杂度为线性时间。它可用于实现洗牌等任务。shufle()的参数是要乱序的范围的首尾途代器,以及一个统一的随机数生成器对象,它指定如何生成随机数。随机数生成器详见第

操作算法

此类算法只有两个: for_each()和 for_each_n(),后者是在 C++17 中引入的。它们对范围内的每个元素执行回调, 或对范围内的前n 个元素执行回调。可利用这两个算法结合简单的函数回调或lambda 表达式执行一些任务,例如打印容器中的每个元素。这里提到这两个算法,是因为可能会在已有的代码中遇到它们,但使用基于区间的 for 循环通常比使用这两个算法更简单、更容易理解。

1.for_each()

下面这个示例使用 lambda 表达式打印 map 中的元素:

map<int, int> myMap = { { 4, 40 }, { 5, 50 }, { 6, 60 } };
for_each(cbegin(myMap),cend(myMap),[](const auto& p){cout<<p.first<<"->"<<p.second<<endl;});

输出

xz@xiaqiu:~/study/test/test$ ./test
4->40
5->50
6->60
xz@xiaqiu:~/study/test/test$

下例说明如何使用 for_each()算法和lambda 表达式,计算范围内元素的和与积。注意,lambda 表达式只显式捕捉需要的变量,它按引用捕捉变量,和否则 lambda 表达式内对 sum 和 product 的修改无法在 lambda 表达式外可见:

vector<int> myVector;
populateContainer(myVector);
int sum = 0;
int product = 1;
for_each(cbegin(myVector),cend(myVector),
[&sum,&product](int i){sum += i;product *= i;
});
cout<<"The sum is "<<sum<<endl;
cout<<"The product is "<<product<<endl;

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):0
The sum is 10
The product is 24
xz@xiaqiu:~/study/test/test$

也可用一个仿函数编写该例,其中,可累加信息,在 for_each()处理完每个元素后检索信息。例如,可编写仿函数 SumAndProduct 同时跟踪,一次性计算元素的和以及运算的积。

class SumAndProduct
{public:void operator()(int value);int getSum() const{ return mSum;}int getProduct() const { return mProduct; }private:int mSum = 0;int mProduct = 1;
};void SumAndProduct::operator()(int value)
{mSum += value;mProduct *= value;
}int main()
{vector<int> myVector;populateContainer(myVector);sumAndProduct func;func = for_each(cbegin(myVector),cend(myVector),func);cout<<"The sum is "<<func.getNum() <<endl;cout<<"The product is "<<func.getProduct()<<endl;return 0;
}

你可能想要忽略 for_each()的返回值,但在调用后仍试图读取 func 的信息。然而,这样做不可行,因为仿函数被复制到 for_ each(),最终从调用返回这个副本。为获得正确的行为,必须捕捉返回值。关于 for_each()和下面讨论的 for_each_n(),最后需要指出的一点是,使用 lambda 或回调时,人允许通过引用获得参数并对其进行修改。这样可以修改实际夫代器范围内的值。本章后面的选民登记例子展示了这项功能的使用。

2.for_each_n()

for_each_n()算法需要范围的起始迭代器、要迭代的元素数量以及函数回调。它返回的迭代器等于 begin + n。它通常不执行任何边界检查。下例只迭代 map 的前两个元素:

map<int,int>myMap = {{4,40},{5,50},{6,60}};
for_each_n(cbegin(myMap),2,[](const auto &p){cout<<p.first<<"->"<<p.second<<endl;
});

交换算法

C++标准库提供了以下交换算法。

  1. swap()

std::swap()用于有效地交换两个值,并使用移动语义(如果可用的话)。它的使用十分简单:

int a = 11;
int b = 22;
cout<<"Before swap(): a = "<<a<<",b = "<<b<<endl;
swap(a,b);
cout<<"After swap(): a ="<<a<<",b="<<b<<endl;

输出如下所示:

Before swap(): a = 11,b = 22
After swap(): a = 22,b = 11
  1. exchange()

std::exchange()在中定义,用新值蔡换旧值,并返回旧值,如下所示;

int a = 11;
int b = 22;
cout<<"Before exchange(): a = "<<a<<", b = "<<b<<endl;
int returnedValue = exchange(a,b);
cout<<"After exchange(): a = "<<a<<",b ="<<b<<endl;
cout<<"exchange() returned: "<<returnedValue<<endl;
  1. exchange()

std::exchange()在中定义,用新值蔡换旧值,并返回旧值,如下所示;

int a = 11;
int b = 22;
cout<<"Before exchange(): a = "<<a<<",b = "<<b<<endl;
int returnedValue = exchange(a,b);
cout<<"After exchange(): a = "<<a<<", b ="<<b<<endl;
cout<<"exchange() returned: "<<returnedValue<<endl;

输出

Before exchange(): a = 11,b = 22
After exchange(): a = 22,b = 22
exchange () returned: 11

exchange()用于实现移动赋值运算符。移动赋值运算符需要将数据从源对象移到目标对象。通常,源对象中的数据会变为 null。通常的做法如下。假设 Foo 有一个数据成员 mPtr,它是指向一些原始内存的指针;

Foo& operator=(Foo& rhs) noexcept
{//check for self-assignmentif(this == &rhs) { return *this;}//Free the old memorydelete mPtr;mPre = nullptr;//Move datamPtr = rhs.mPtr; //Move data from source to destinationrhs.mPtr = nullptr; //Nullfy data in sourcereturn *this;
}

对于方法底部的 mPtr 和 rhs.mPtr 的赋值,可使用 exchange()实现,如下所示:

Foo& operator=(Foo&& rhs) noexcept
{//check for self-assignmentif(this == &rhs) { return *this; }//Free the old memorydelete mPtr;mPtr = nullptr;//Move datamPtr = exchange(rhs.mPtr,nullptr); //Move + nullfyreturn *this;
}

分区算法

partition_copy()算法将来自某个来源的元素复制到两个不同的目标。为每个元素选择特定目标的依据是谓词的结果: true 或 false。partition_copy()的返回值是一对迭代器: 一个迭代器引用第一个目标范围内最后复制的那个元素的后一个位置(one-past-the-last-copied element),另一个迭代器引用第二个目标范围内最后复制的那个元素的后一个位置。将这些返回的迭代器与 erase()结合使用,可删除两个目标范围内多余的元素,就像之前的 copy_if()示例那样。下例要求用户输入一些整数,然后将这些整数分区到两个目标 vector 中,一个保存偶数,另一个保存奇数。

vector<int> vec1,vecOdd,vecEven;
populaContainer(vec1);
vecOdd.resize(size(vec1));
vecEven.resize(size(vec1));
auto pairIters = pairtition_copy(cbegin(vec1),cend(vec1),begin(vecEven),begin(vecOdd),[](int i){ return i % 2 == 0;})
vecEven.erase(pairIters.first,end(vecEven));
vecOdd.erase(pairIters.second,end(vecOdd));cout<<"Event numbers: ";
for(const auto& i : vecEven){cout<<i<<" ";}
cout<<endl<<"Odd numbers: ";
for(const auto& i : vecOdd){cout<<i<<" ";}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):0
Event numbers: 2 4 2
Odd numbers: 1 3 1

排序算法

标准库提供了一些不同的排序算法。“排序算法”重新排列容器中元素的顺序,使集合中的元素保持连续顺序。因此,排序算法只能应用于顺序集合。排序和有序关联容器无关,因为有序关联容器已经维护了元素的顺序。 排序也和无序关联容器无关,因为无序关联容器就没有排序的概念。一些容器(例如 list 和 forward_list提供了自己的排序方法,因为这些方法内部实现的效率比通用排序机制的效率要高。因此,通用的排序算法最适用于 vector、deque、array 和 C 风格数组。sort()国数一般情况下在 O(N log N)时间内对某个范围内的元素排序。将 sort()应用于一个范围之后,根据运算符 operator<,这个范围内的元素以非递减顺序排列(最低到最高)。如果不希望使用这个顺序,可以指定一个不同的比较回调,例如 greater。

sort()函数的一个名为 stable_sort()的变体能保持范围内相等元素的相对顺序。然而,由于这个算法需要维护范围内相等元素的相对顺序,因此这个算法比 sort()算法低效。下面是 sort()算法的一个示例:

vector<int> vec;
populateContainer(vec)l
sort(begin(vec),end(vec),greater<>());

还有is_sorted()和 is_sorted_until(); 如果给定的范围是有序的,is_sorted()就返回 true,而 is_sorted_until()返回给定范围内的一个迭代器,该欠代器之前的所有元素都是有序的。

二叉树搜索算法

有几个搜索算法只用于有序序列或至少已分区的元素序列。这些算法有 binary_search()、lower_bound()、upper bound()和 equal_range()。lower_ bound()、upper_bound()和 equal_range()算法类似于 map 和 set 容器中的对应方法。用法示例参见第 17 章。lower bound()算法在有序范围内查找不小于(即大于或等于)给定值的第一个元素,经常用于发现在有序的vector 中应将新值插入哪个位置,使 vector 依然有序。下面是一个示例:

vector<int> vec;
populateContainer(vec);//Sort the container
sort(begin(vec),end(vec));
cout<<"Sorted vector: ";
for(const auto& i : vec){ cout<<i<<" ";}
cout<<endl;
while(true)
{int num;cout<<"Enter a number to insert (0 to quit): ";cin>>num;if(num == 0)break;auto iter = lower_bound(begin(vec),end(vec),num);vec.insert(iter,num);cout<<"New vector:";for(const auto& i : vec){cout<<i<<" ";}cout<<endl;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a number (0 to quit):1
Enter a number (0 to quit):1
Enter a number (0 to quit):0
Sorted vector: 1 1
Enter a number to insert (0 to quit): 1
New vector:1 1 1
Enter a number to insert (0 to quit): 1
New vector:1 1 1 1
Enter a number to insert (0 to quit): 1
New vector:1 1 1 1 1
Enter a number to insert (0 to quit): 2
New vector:1 1 1 1 1 2
Enter a number to insert (0 to quit): 2
New vector:1 1 1 1 1 2 2
Enter a number to insert (0 to quit): 3
New vector:1 1 1 1 1 2 2 3
Enter a number to insert (0 to quit):

binary search()算法以对数时间而不是线性时间搜索元素,需要指定范围的首尾迭代器、要搜索的值以及可选的比较回调。如果在指定范围内找到这个值,这个算法返回 true,和否则返回 false。下面的例子演示了这个算法:

vector<int> vec;
populateContainer(vec);//Sort the container
sort(begin(vec),end(vec));while(true)
{int num;cout<<"Enter a number to find (0 to quit);";cin>>num;if(num == 0){break;}if(binary_search(cbegin(vec),cend(vec),num)){cout<<"That number is in the vector ."<<endl;}else{cout<<"That number is not in the vector "<<endl;}
}

集合算法

集合算法可用于任意有序范围。includes()算法实现了标准的子集判断功能,检查某个有序范围内的所有元素是否包含在另一个有序范围内,顺序任意。set_union()、set_intersection()、set_difference()和 set_symmetric_difference()算法实现了这些操作的标准语义。在集合论中, 并集得到的结果是两个集合中的所有元素。交集得到的结果是所有同时存在于两个集合中的元素。差集得到的结果是所有存在于第一个集合中,但是不存在于第二个集合中的元素。对称差集得到的结果是两个集合的“异或“: 所有存在于其中一个集合中,但不同时存在于两个集合中的元素。

警告;

务必确保结果范围足够大,以保存操作的结果。对于 set_union()和 set_ symmetric_difference(),结果大小的上限是两个输入范围的总和。对于 set_intersection(),结果大小的上限是两个输入范围的最小大小。对于set_difference(),结果大小的上限是第一个输入范围的大小。

警告;

不能使用关联容器(包括 set)中的迭代器范围来保存结果,因为这些容器不允许修改键。

下面是这些算法的使用示例;

#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;template<typename Container>
void populateContainer(Container &cont)
{int num;while (true){cout << "Enter a number (0 to quit):";cin >> num;if (num == 0){break;}cont.push_back(num);}
}template <typename Iterator>
void DumpRange(string_view message,Iterator begin,Iterator end)
{cout<<message;for_each(begin,end,[](const auto& element){ cout<< element<<" ";});cout<<endl;
}int main(int argc, char *argv[])
{vector<int> vec1, vec2, result;cout << "Enter elements for set 1:" << endl;populateContainer(vec1);cout << "Enter elements for set 2:" << endl;populateContainer(vec2);//set algorithms work on sorted rangessort(begin(vec1), end(vec1));sort(begin(vec2), end(vec2));DumpRange("Set 1: ", cbegin(vec1), cend(vec1));DumpRange("Set 2: ", cbegin(vec2), cend(vec2));if (includes(cbegin(vec1), cend(vec1), cbegin(vec2), cend(vec2))){cout << "The second set is a subset of the first " << endl;}if (includes(cbegin(vec2), cend(vec2), cbegin(vec1), cend(vec1))){cout << "The first set is a subset of the second " << endl;}result.resize(size(vec1) + size(vec2));auto newEnd = set_union(cbegin(vec1), cend(vec1), cbegin(vec2), cend(vec2), begin(result));DumpRange("The union is:", begin(result), newEnd);newEnd = set_intersection(cbegin(vec1), cend(vec1), cbegin(vec2), cend(vec2), begin(result));DumpRange("The union is: ",begin(result), newEnd);newEnd = set_difference(cbegin(vec1), cend(vec1), cbegin(vec2), cend(vec2), begin(result));DumpRange("The differenc between set 1 and 2 is: ", begin(result), newEnd);newEnd = set_symmetric_difference(cbegin(vec1), cend(vec1), cbegin(vec2), cend(vec2), begin(result));DumpRange("The symmetric difference is: ", begin(result), newEnd);    return 0;}

DumpRange()是一个小型的辅助函数模板,可将给定范围内的元素写入标准输出流。实现方式如下:

template <typename Iterator>
void DumpRange(string_view message,Iterator begin,iterator end)
{cout<<message;for_each(begin,end,[](const auto& element){ cout<< element<<" ";});cout<<endl;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter elements for set 1:
Enter a number (0 to quit):1
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):1
Enter a number (0 to quit):0
Enter elements for set 2:
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):8
Enter a number (0 to quit):0
Set 1: 1 1 1 2 3 4
Set 2: 1 2 3 4 8
The union is:1 1 1 2 3 4 8
The union is: 1 2 3 4
The differenc between set 1 and 2 is: 1 1
The symmetric difference is: 1 1 8
xz@xiaqiu:~/study/test/test$

merge()算法可将两个排好序的范围归并在一起,并保持排好的顺序。结果是一个包含两个源范围内所有元素的有序范围。这个算法的复杂度为线性时间。这个算法需要以下参数,

e 第一个源范围的首尾迭代器

e 第二个源范围的首尾迭代器

e 目标范围的起始和迭代器

e (可选)比较回调

如果没有 merge(),还可通过串联两个范围,然后对串联的结果应用 sort(),以达到同样的目的,但这样做的效率更低,复杂度为 O(N log N)而不是 merge()的线性复杂度。

警告,

一定要确保提供足够大的目标范围,以保存归并的结果。

下例演示了 merge()算法:

vector<int> vectorOne,vectorTwo,vectorMerged;
cout<<"Enter values for first vector:"<<endl;
populateContainer(vectorOne);
cout<<"Enter values for second vector:"<<endl;
populateContainer(vectorTwo);
//Sort both containers
sort(begin(vectorOne),end(vectorOne));
sort(begin(vectorTwo),end(vectorTwo));
//Make sure the destination vector is large enough to hold the values
//from both source vectors
vectorMerged.resize(size(vectorOne) + size(vectorTwo));
merge(cbegin(vectorOne),cend(vectorOne),cbegin(vectorTwo),cend(vectorTwo),begin(vectorMerged));
DumpRange("Merged vector: ",cbegin(vectorMerged),cend(vectorMerged));

最大/最小算法

min()和 max()算法通过运算符 operator<或用户提供的二元谓词比较两个或多个任意类型的元素,分别返回一个引用较小或较大元素的 const 引用。minmax()算法返回一个包含两个或多个元素中最小值和最大值的 pair。这些算法不接收迭代器参数。还有使用迭代器范围的 min_element()、max_element()和 minmax_element()。下面的程序给出了一些示例

int x = 4,y = 5;
cout<<"x is "<<x<<" and y is "<<y<<endl;
cout<<"Max is "<<max(x,y)<<endl;
cout<<"Min is "<<min(x,y)<<endl;//Using max() and min() on more than two values
int x1 = 2,x2 = 9,x2 = 3,x4 = 12;
cout<<"Max of 4 elements is "<<max({x1,x2,x3,x4})<<endl;
cout<<"Min of 4 elements is "<<min({x1,x2,x3,x4})<<endl;//Using minmax
auto p2 = minmax({x1,x2,x3,x4});
cout<<"Minmax of 4 elements is<" <<p2.first<<","<<p2.second<<">"<<endl;//Using minmax() * c++17 structured bindings
auto[min1,max1] = minmax({x1,x2,x3,x4});
cout<<"minmax_element() result: <"<<*min1<<","<<*max1<<">"<<endl;//Using minmax_element() + c++ 17 structured bindings
vector<int> vec{11,33,22};
auto[min2,max2] = minmax_element(cbegin(vec),cend(vec));
cout<<"minmax_element() result: <"<<min2<<","<<*max2<<">"<<endl;

输出

xz@xiaqiu:~/study/test/test$ ./test
x is 4 and y is 5
Max is 5
Min is 4
Max of 4 elements is 12
Min of 4 elements is 2
Minmax of 4 elements is<2,12>
minmax_element() result: <2,12>
minmax_element() result: <11,33>
xz@xiaqiu:~/study/test/test$

注意:

你有时可能遇到查找最大最小值的非标准宏. 例如 GNU C Library (glibc)有宏 MIN()和 MAX(),Windows.h头文件定义了宏 min()和 max(),因为它们是宏,所以可能对其参数进行二次求值; 而 std::min()和 std::max()对每个参数正好进行一次求值。确保总是使用 C++版本的 std::min()和 std::max()。当需要使用 std::min()和 std::max()时,这些 min()和 max()宏可能会干扰你。此时,可再加一对括号来禁用 宏,如下所示:

auto maxValue = (std::max)(1,2);

​ std::clamp()是一个小型辅助函数, 在中定义, 可用于确保值(v)在给定的最小值(lo)和最大值(hi)之间。如果v<lo,它返回对 lo 的引用; 如果v> hi,它返回对hi 的引用,和否则返回对 v 的引用。下面是一个例子

cout << clamp(-3,-1,10)<<endl;
cout << clamp(3,1,10)<<endl;
cout << clamp(22,1,10)<<endl;

输出如下

1
3
10

并行算法

对于 60 多种标准库算法,C++17 支持并行执行它们以提高性能,示例包括 for_each()、all_of()、copy()、count_if(),find(),replace(),search(),sort()和transform()等,支持并行执行的算法包含选项,接收所谓的执行策略作为第一个参数。执行策略允许指定是否允许算法以并行方式或矢量方式执行。有三类标准执行策略,以及这些类型的三个全局实例,它们全部定义在 std::execution 名称空间的头文件中。如表 18-1 所示。

执行策略类型

执行策略类型 全局实例 描述
sequenced_policy seq 不允许算法并行执行
parallel_policy par 允许算法并行执行
parallel_unsequenced_policy par_unseq 允许算法并行执行和矢量化执行,还允许在线程之问迁移执行

也可以给标准库实现添加其他执行策略。注意,使算法使用 parallel_unsequenced_policy 执行策略,以允许对回调进行交错函数调用,即不按顺序执行,这意味着会对函数回调施加诸多限制。例如,不能分配/释放内存、获取互斥以及使用非锁 std::atomics(见第 23 章)等。对于其他标准策略,函数调用按顺序执行,但顺序无法确定。此类策略不会对函数调用操作

并行算法未采取措施来避免数据争用和死锁,在并行执行算法时,由你来设法避免此类情况。第 23 章将讨论如何避免数据争用和死锁。下例使用并行策略,对 vector 的内容进行排序:

sort(std::execution::par,begin(myVector),end(myVector));

数值处理算法

前面介绍了数值处理算法的一个示例: accumulate()。本节介绍其他几个数值算法的示例。

1.inner_product()

中定义的 inner_product()算法计算两个序列的内积,例如,下面程序中的内积计算为(1*9)+(2*8)+(3*7)+(4*6): ,

vector<int> v1{1,2,3,4};
vector<int> v2{9,8,7,6};
cout<<inner_product(cbegin(v1),cend(v1),cbegin(v2),0)<<endl;

输出70

2.iota()

头文件中定义的 iota()算法会生成指定范围内的序列值,从给定的值开始, 并应用 operator+来生成每个后续值。下面的例子展示了如何将这个新算法用于整数的 vector,不过要注意这个算法可用于任意实现了 operator++的元素类型,

vector<int> vec(10);
iota(begin(vec),end(vec),5);
for(auto& i : vec){ cout<<i<<" ";}

输出如下所示:

xz@xiaqiu:~/study/test/test$ ./test
5 6 7 8 9 10 11 12 13 14

​ 3. gcd()和lcm()

gcd()算法返回两个整数的最大公约数,而 lcm()算法返回两个整数的最小公倍数。 它们都定义在中。下面是一个示例

auto g = gcd(3,18); //g = 3;
auto l = lcm(3,18); //l = 18
  1. reduce()

不支持并行执行的算法很少,std::accumulate()就是其中之一。相反,需要使用新引入的 std::reduce()算法,通过并行执行选项,计算广义和。例如,以下两行同样是求和,但是 reduce()以并行和矢量化方式运行,因此速度更快,对于大型输入范围尤其如此:

double result1 = std::accumulate(cbegin(vec),cend(vc),0.0);
double result2 = std::reduce(std::execution::par_unseq,cbegin(vec),cend(vec));

-般而言,accumulate()和 reduce()计算[x0,xn]范围内元素的和,初始值为 init,并且给定了二元运算符θ\thetaθ:
Init θx0θx1θ…⊗xn−1\text { Init } \theta x_{0} \theta x_{1} \theta \ldots \otimes x_{n-1}  Init θx0​θx1​θ…⊗xn−1​
5.transform_reduce()

std::inner product()是另一个不支持并行执行的算法。相反,需要使用广义的 transform_reduce()算法,它具有并行执行选项,可用于计算内积等。它的用法与 reduce()类似,因此这里不再列举示例。

  1. 扫描算法

C++17 引入了 4 个扫描算法: exclusive_scan()、inclusive_scan()、transform_exclusive_scan0和 transform_inclusive_scan()。

表 18-2 中,针对[x0, xn]元素范围,由 exclusive_scan()和 inclusive_scan()/partial_sum()计算和[yo yn],初始值为 Init(partial_sum()为 0),给定运算符Θ\ThetaΘ。

transform_exclusive_ scan()和 transform_inclusive_scan()在计算广义和之前,都首先给元素应用一元函数,这类似于 transform_reduce()在执行前给元素应用一元函数。注意, 这些扫描算法可接收可选的执行策略, 以并行地执行。这些扫描算法的计算顺序不确定, partial_sum()和 accumnulate()的顺序是从左到右,正因为如此,partial_sum()和 accumulate()无法并行化!

18.6 算法示例: 审核选民登记

有些人尝试在两个或多个不同的投票地区登记并投票。此外,还有一些人(例如罪犯)没有资格投票,但还是会试图登记并投票。利用新掌握的算法技能,编写一个简单的选民登记审核函数,在选民册中检查违规情况。

18.6.1 选民登记审核问题描述

选民登记审核函数应该审核选民的信息。 假设选民注册信息根据地区保存在一个 map 中, 这个 map 将地区名映射至选民的 vector。审核函数应该接收这个 map 和罪犯的 vector 作为参数,并将所有罪犯从选民 vector 中删除。此外,这个函数还应该找出所有在一个以上地区登记的选民,并将这些选民从所有地区删除。应该将带有多重登记信息的选民的所有登记信息删除, 使这些选民无权参与选举。为简单起见, 假定选民 vector 只是 string类型的姓名 vector。真实程序显然要求更多数据,例如地址和党派关系信息。

auditVoterRolls()函数

auditVoterRolls()函数按照以下 3 个步骤工作:

(1) 调用 getDuplicates(),获得所有登记 vector 中的重复姓名。

(2) 将重复姓名 set 和罪犯 vector 合并。

(3) 在每个选民 vector 中删除重复姓名 set 和罪犯 vector 合并结果中的所有姓名。这里采用的方法是通过for_each()处理 map 中的每个 vector,应用 lambda 表达式从每个 vector 中移除违规选民的姓名。以下是代码中使用的类型别名:

using VoterMap = map<string,vector<string>>;
using DistrictPair = pair<const string,vector<string>>;

下面是 auditVoterRolls()的实现:;

//Expects a map of string/vector<string> pairs keyed on district name
//and containing vectors any name on the convictedFelons in those districts
//Removes from each vector any name on the convictedFelons vector and
//any name that is found on any other vector
void auditVoterRolls(VotersMap& votersByDistric,const vector<string>& convictedFelons)
{//Get all the duplicate namesset<string> toRemove = getDuplicates(votersByDistriet);//Combine the duplicates and convicted felons -- we want//to remove names on both vectors from all voter rollstoRemove.insert(cbegin(convictedFelons),cend(convictedFelons));//Now remove all the names we need to remove using//nested lambda expressions and the remove-erase-idiomfor_each(begin(votersByDistrict),end(votersByDistrict),[&toRemove](DistrictByDistrict& district)){auto it = remove_if(begin(district.second),end(district.second),[&toRemove](const string& name){ return (toRemove.count(name) > 0);};)district.second.erase(it,end(district.second));}
}

该实现使用 for_each()算法来演示其用法。当然,也可以不使用 for_each(),而使用如下基于范围的 for 循环(也使用了 C++17 结构化绑定);

for(auto& [district,voters] : votersByDistrict)
{auto it = remove_if(begin(voters),end(voters),[&toRemove](const string& name){return (toRemove.count(name)>0);});voters.erase(it,end(voters));
}

getDuplicates()函数

getDuplicates()函数必须找到所有出现在多个选民登记 vector 中的姓名。为解决这个问题,可采取几种不同的方法。为演示 adjacent_find()算法,这个实现将每个地区的 vector 合并为一个大的 vector,并对这个 vector 排序。 排序后, 不同 vector 中的重名都会在这个大的 vector 中出现并且相邻。现在,可对这个大的排好序的 vector应用 adjacent_find()算法,找到所有连续出现的重名,并将它们保存在一个名为 duplicates 的 set 中。下面是实现代码:

//Returns a set of all names that appear in more than one vector in the map
set<string> getDuplicates(const VotersMap& votersByDistrict)
{//Collect all the names from all the vectors into one big vectorvector<string> allNames;for(auto & district : votersByDistrict){allNames.insert(end(allNames),begin(district.second),end(district.second));}//Sort the vectorsort(begin(allNames),end(allNames));//Now it's sorted,all duplicate names will be next to each other//Use adjacent_find() to find instances of two or more identical names//next to each other//Loop until adjacent_find() returns the end iteratorset<string> duplicates;for(auto lit = cbegin(allNames);lit != cend(allNames);++lit){lit = adjacent_find(lit,cend(allNames));if(lit == cend(allNames))break;duplicates.insert(*it);}return duplicates;
}

在这个实现中, alINames 的类型为 vector。 这样, 这个例子就能展示如何使用 sort()和 adjacent find()算法。另一种解决方案是将 allNames 的类型改成 set,这样可得到更简洁的实现,因为 set 不允许重复。这个新的解决方案循环遍历所有 vector,并试图将每个姓名插入 allNames。当插入失败时,意味着 alNames 中已存在那个姓名的元素,所以把这个姓名添加到 duplicates 中。注意,代码中使用了 C++17 结构化绑定。

set<string> getDuplicates(const VotersMap& votersByDistrict)
{set<string> allNames;set<string> duplicates;for(auto& [district,voters] : votersByDistrict){if(!allNames.insert(name).second){duplicates.insert(name);}}return duplictes;
}

18.6.4 测试 auditVoterRolls()函数

以上是选民登记审核功能的完整实现。下面是一个简单的测试程序:

/ Initialize map using uniform initialization
VotersMap voters =
{{"Orange", {"Amy Aardvark", "Bob Buffalo","Charles Cat", "Dwayne Dog"}},{"Los Angeles", {"Elizabeth Elephant", "Fred Flamingo","Amy Aardvark"}},{"San Diego", {"George Goose", "Heidi Hen", "Fred Flamingo"}}
};
vector<string> felons = {"Bob Buffalo", "Charles Cat"};//Local lambda expression to print a district
auto printDistrict = [](const DistrictPair& district)
{cout<<district.first<<":";for(auto& str : district.second){cout<<" {" << str<< "}";}cout<<endl;
};
cout<<"Before Audit:"<<endl;
for(const auto & district : voters) { printDistrict(district);}
cout<<endl;auditVoterRolls(voters,felons);cout<<"After Audit: "<<endl;
for(const auto & district : voters){printDistrict(district);}
cout<<endl;

字符串的本地化与正则表达式

本地化

在学习 C 或 C++编程时,为方便学习, 将字符等同于字节, 把所有字符当成ASCI[(美国标准信息交换代码,American Standard Code for Information Interchange)字符集中的成员。ASCII是一个7 位的集合,通常保存在8位的 char 类型中。在现实中,富有经验的 C++程序员意识到,成功的程序应该世界通用。即使程序一开始没有考虑到国际用户,也不应该在日后不考虑本地化或软件对本地语言的支持。

注意

本章简要介绍本地化、不同字符编码以及字符串代码的可移植性。本书不详细讨论所有这些主题,无论要讲清楚其中的哪个主题,都需要一整本书。19.1.1 本地化字符串字面量

本地化的一个关键点在于绝不能在源代码中放置任何母语的字符串字面量,除非是面向开发人员的调试字符串。在 Microsoft Windows 应用程序中,通过将字符串放在 STRINGTABLE 资源中达到了这个目的。其他大部分平台都提供了类似的功能。如果需要将应用程序翻译为其他语言,只要翻译那些资源即可,而不需要修改任何源代码。有一些工具可以帮助完成翻译过程。为让源代码能本地化,不应该利用字符串字面量组成句子,即使单独的字面量也可以被本地化。例如:

cout << "Read " << n << " bytes" << endl;

这条语句不能本地化为荷兰语,因为荷兰语的语序有所变化。荷兰语的翻译应该为

cout << n << " bytes gelezen" << endl;

为能正确地本地化这个字符串,可采用下面的方式来实现

cout << Format (IDS_TRANSFERRED,n) << endl;

IDS_TRANSFERRED 是字符串资源表中一个条目的名称。对于英文版,IDS_TRANSFERRED 可定义为"Read $1 bytes"; 对于荷兰语版,这条资源可以定义为"$1 bytes gelezen"。Format(O)函数加载字符串资源,并将$1替换为n 的值。

19.1.2 ”宽字符

用字节表示字符的问题在于,并不是所有的语言(或字符集)都可以用 8 位(即 1个字节)来表示。C++有一种内建类型 wchar_t,可以保存宽字符(wide characten)。带有非 ASCII(U.S.)字符的语言,例如日语和阿拉伯语,在 C++中可以用 wchar_t 表示。然而,C++标准并没有定义 wchar_t 的大小。一些编译器使用 16 位,而另一些编译器使用 32 位。为编写跨平台代码,将 wchar_t 假定为任何特定的数值都是不安全的。如果软件可能会用在非西方字符集的环境中(注意: 一定会有! ),和那么应该从一开始就使用宽字符。在使用 wchar { 时,需要在字符串和字符字面量的前面如上字母 L,以表示应该使用宽字符编码。 例如, 要将 wchar {字符初始化为字母 m,应该编写以下代码:

wchar_t myWideCharacter = L'm';

大部分常用类型和类都有宽字符版本。宽字符版本的 string 类为 wstring。“ 前缀字母 w”模式也可以应用于流。wofstream 处理宽字符文件输出流,wifstream 处理宽字符文件输入流。cout、cin、cerr 和 clog 也有宽字节版本: wcout、wcin、wcerr 和 wclog。这些版本的使用和非宽字节版本的使用没有区别;

wcout << L"I am a wide-character string literal." << endl;

非西方字符集

宽字符是很大的进步,因为宽字符增加了定义一个字符可用的空间。下一步是要解决如何利用这个空间的问题。在宽字符集中,和 ASCII 一样,字符用编号表示,现在称为码点。唯一的区别在于编号不能放在 8 个位中。字符到码点的映射要大得多,因为这个映射除了能够处理英语为母语的程序员所熟悉的字符外,还处理很多不同的字符集。国际标准 ISO 10646 定义的 Universal Character Set (UCS)和 Unicode 都是标准化的字符集。 这些字符集包含大约 10 万个抽象字符, 每个字符都由一个无歧义的名字和一个码点标识。 两个标准中都有带有同样编号的相同字符,并且都有可以使用的特定编码。例如,UTF-8 是 Unicode 编码的一个实例,其中 Unicode 字符编码为 1到4个8位字节,UTF-16 将 Unicode 字符编码为一个或两个 16 位的值,UTF-32 将 Unicode 字符编码为正好32 位。不同应用程序可使用不同编码。遗憾的是,C++标准并没有定义宽字符 wchar_t 的大小。在 Windows 平台上为 16 位,在其他平台上可能为 32 位。在使用宽字符编写跨平台代码时,应该注意到这一点。为解决这个问题,可使用另外两个字符类型, char16_t 和 char32_t。下面的列表总结了支持的所有字符类型 。

使用字符串前缀可将字符串字面量转换为特定类型。下面列出所有支持的字符串前绥。

  1. char: 存储8 个位。可用于保存 ASCII 字符, 还可用作保存 UTF-8 编码的 Unicode 字符的基本构建块。使用 UTF-8 时,一个 Unicode 字符编码为 1 到 4 个 char。

  2. char16_t: 存储 16 个位。可用作保存 UTF-16 编码的 Unicode 字符的基本构建块。其中,一个 Unicode字符编码为一个或两个 char16_t。

  3. char32_t: 存储至少 32 个位。可用于保存 UTF-32 编码的 Unicode 字符,每个字符编码为一个 char32_t。

  4. wchar_t: 保存一个宽字符,宽字符的大小和编码取决于编译器。使用 char16_t 和 char32_t 而不是 wchar_t 的好处在于: char16_t 的大小至少 16 位,char32_t 的大小至少 32位,它们的大小和编译器无关,而 wchar_t 不能保证最小的大小。

C++标准还定义了以下两个宏。

  1. _STDC_UTF_32__: 如果编译器定义了这个宏,那么类型 char32_t 使用 UTF-32 编码。如果没有定义这个宏,那么类型 char32_t 使用与编译器相关的编码。

  2. _STDC_UTF_16__: 如果编译器定义了这个宏,那么类型 char16_t 使用 UTF-16 编码。如果没有定义这个宏,那么类型 char16_t 使用与编译器相关的编码。

  3. u8: 采用 UTF-8 编码的 char 字符串字面量。

  4. u: 表示 char16_t 字符串字面量,如果编译器定义了_STDC_UTF_16__宏,则表示 UTF-16 编码。

  5. U: 表示 char32_t字符串字面量,如果编译器定义了_STDC_UTF_32__宏,则表示 UTF-32 编码。

  6. L: 采用编译器相关编码的 wchar_t 字符串字面量。

所有这些字符串字面量都可与第 2 章介绍的原始字符串字面量前缀 R 结合使用。例如:

const char* s1 = u8R"(Raw UTF-8 encoded string literal)";
const wchar_t* s2 = LR"(Raw wide string literal)";
const char16_t* s3 = uR"(Raw char16_t string literal)";
const char32_t* s4 = UR"(Raw char32_t string literal)";

如果通过 u8 UTF-8 字符串字面量使用了 Unicode 编码,或者编译器定义了_STDC_UTF_16__ 或_STDC_UTF_32__宏,那么在非原始字符串字面量中可通过\uABCD 符号插入指定的 Unicode 码点。例如\u03C0 表示 pi 字符,\u00B2 表示字符2^22,因此以下代码会打印出“πr2\pi r^2πr2”:

const char* formula = u8"\u03C0 r\u00B2";

与此类似,字符字面量也可具有前缀,以转换为特定类型。C++17 支持前缀 u、U 和 L,而且为字符串字面量添加了 u8 前缀。一些例子有: u‘a’、U’a’、L’a’和 u8’a’。

除 std::string 类外,目前还支持 wstring、ul6string 和 u32string。它们的定义如下:

➤ ➤ using string = basic_string<char>;
➤ ➤ using wstring = basic_string<wchar_t>;
➤ ➤ using u16string = basic_string<char16_t>;
➤ ➤ using u32string = basic_string<char32_t>;

多字节字符由一个或多个依赖编译器编码的字节组成,类似于 Unicode 通过 UTF-8 用 1 到 4 个字节表示,或者通过 UTF-16 用一个或两个 16 位值表示。下面的转换函数可在 char16_t char32_t 和多字节字符之间来回转换: mbrtoc16、c16rtomb 、mbrtoc32 和 c32rtomb。遗憾的是,对 char16_t 和 char32_t 的支持就这么多了。有一些转换类可供使用(参见 19.1.4 节),但数量不多。例如,并没有支持 char16_t和 char32_t 的 cout 或 cin 版本,因此很难向控制台打印这种字符串,或从用户输入读入这种字符串。如果想要更多地使用 charl16_t 和 char32_t 字符串,那么需要求助于第三方库。ICU(nternational Components for Unicode)是一个十分知名的库,可为应用程序提供 Unicode 和全球化支持。

转换

C++标准提供 codecvt 类模板,以帮助在不同编码之间转换。头文件定义了如表 19-1 所示的 4个编码转换类。

描述
codecvt<char,char,mbstate_t> 恒等转换,也就是无转换
codecvt<char16_t,char,mbstate_t> UTF-16 和 UTF-8 之间的转换
codecvt<char32_t,char,mbstate_t> UTF-32 和 UTF-8 之间的转换
codecvt<wchar_t,char,mbstate_ t> 宽字符编码(取决于实现)与窗字符编码之间的转换

忆 在 C++17 之前,中定义了以下三种代码转换: codecvt_utfg、codecvt_utfl6 和 codecvt_utfg_utfl6。可通过两种简便的转换接口使用它们: wstring_convert 和 wbuffer_ convert。C++17 不赞成使用这三个转换(整个头文件)和这两个简便接口,因此本书不再讨论。C++标准委员会决定不再使用该功能,因为它不能正确地处理错误。结构有误的 Unicode 字符串会带来安全风险,实际上,它们已被用作危害系统安全的攻击矢量。另外,API 过于隐涩,难以理解。在 C++标准委员会提出恰当的、安全的、易用的功能来替换不赞成使用的功能前,建议使用第三方库(如 ICU)来正确处理 Unicode 字符串。

locale 和 facet

不同国家间的数据表示中,字符集并不是唯一的不同之处。即使使用相似字符集的国家之间,例如英国和美国,也会存在数据表示的不同,例如日期和货币。标准的 C++中,将一组特定的文化参数相关的数据组合起来的机制称为locale。locale 中的独立组件,例如日期格式、时间格式和数字格式等称为 facet。U.S. English 是一个 locale 实例。显示日期时采用的格式是一个 包cet实例。有一些内建的 facet 是所有 locale 共用的。C++语言还提供了一种自定义和添加 facet 的方式。

  1. 使用 locale

使用 IO 流时,根据特定的 locale 对数据进行格式化。locale 是可以关联到流的对象。locale 在头文件中定义。locale 的名字和具体的实现相关。POSIX 标准将语言和区域分隔在两字母的段中,再加上可选的编码。例如,美国使用的英语语言 locale 为en_US,而英国使用的英语语言 locale 为en_GB。日本使用的日语再加上 Japanese Industrial Standard 编码的 locale 为ja_JP.jis。Windows 上的 locale 名称使用不同的格式。首选格式与 POSIX 格式十分类似,但用虚线替代下划线。次选格式是旧格式,如下所示:

lang[_country_region[.code_page]]

方括号内的内容是可选的。表 19-2 列出了一些示例。

POSIX Windows Window(旧式)
U.S. English en_US en-US English_United States
Great Britain English en_GB en-GB English_Great Britain

大部分操作系统都提供了一种根据用户的定义判断 locale 的机制。在 C++中, 向 std::locale 对象的构造函数传入一个空的字符串,可以根据用户的环境创建 locale。一旦创建这个对象,就可以用它查询 locale,根据它做出一些程序的判断。下面的代码演示了如何在流上调用 imbue()方法,使用用户的 locale。结果就是所有发送到wconut 的内容都会根据环境的格式化规则进行格式化。

wcout.imbue(locale(""));
wcout<<23767<<endl;

这意味着如果系统 locale 为美式英语,那么输出数字 32767 时会显示为 32,767; 如果系统 locale 为比利时荷兰语,那么同样的数字会显示为 32.767。默认 locale 通常是经典 locale,不是用户的 locale。经典 locale 使用 ANSI C 风格的约定。经典 C locale 类似于 U.S. English,但有一些细微区别。例如,在输出数字时不会带任何标点符号:

wcout.imbue(locale("C"));
wcout<<23767<<endl;

这段代码的输出如下所示:

xz@xiaqiu:~/study/test/test$ ./test
23767
xz@xiaqiu:~/study/test/test$

以下代码手工设置了美式英语 locale,因此数字 32767 会通过美式英语标点格式化,与系统 locale 无关:

wcout.imbue(locale("en-US"));
wcout<<23767<<endl;

输出

xz@xiaqiu:~/study/test/test$ ./test
terminate called after throwing an instance of 'std::runtime_error'what():  locale::facet::_S_create_c_locale name not valid
已放弃 (核心已转储)
xz@xiaqiu:~/study/test/test$

不知道什么原因

可通过 locale 对象来查询 locale 的信息。例如,下面的程序创建了一个匹配用户环境的 locale。通过 name()方法可得到描述这个 locale 的 C++字符串。然后,通过 find()方法在这个字符串中查找指定的子串。如果没有找到指定的子串,则返回 string::npos。这段代码检查 Windows 标准和 POSIX 标准的名称。根据 locale 是否为美式英语,这段程序输出两条信息中的一条:

local loc("");
if(loc.name().find("en_US") == string::npos && loc.name().find("en-US") == string::npos)
{wcout<<L"Welcome non-U.S. english speaker!"<<endl;
}
else
{wcout<<L"Welcom U.S. Enginlis speaker!"<<endl;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Welcome non-U.S. english speaker!
xz@xiaqiu:~/study/test/test$

注意;

如果要将数据写入一个文件,而程序将从这个文件读回数据,建议使用中性的 C locale; 否则,将很难解析。另外,在用户界面中显示数据时,建议根据用户 locale 设置数据格式。

  1. 字符分类

头文件包含以下字符分类函数: std::isspace()、isblank()、iscntrl()、isupper()、islower()、isalpha()、isdigit()、ispunct()、isxdigit()、isalnum()、isprint()和 isgraph()。它们都接收两个参数:要分类的字符,以及用于分类的 locale。下面的 isupper()示例使用用户的环境 locale:

bool result = isupper('A',locale(""));
  1. 字符转换

头文件也定义了两个字符转换函数: std::toupper()和 tolower()。它们接收两个参数: 要转换的字符,以及用于转换的 locale。

  1. 使用 facet

通过 std::use_facet()函数可获得特定 locale 中的某个特定 facet。use_facet()的参数是 locale。 例如,以下表达式通过 POSIX 标准的 locale 名称获得英式英语 locale 中的标准货币符号 facet:

use_facet<moneypunct<wchar_t>>(locale("en_GB"));

注意,最内层的模板类型决定了要使用的字符类型。通常使用的是 wchar_t 或 char。嵌套模板类的使用不合时宜,但不要管这些语法,其结果包含英国货币符号相关的所有信息。标准 facet 中的数据定义在头文件及其关联的文件中。 表 19-3 列出了标准中定义的标准 facet 类别。可参阅标准库参考资源, 以了解各个 facet的详情。

下面的程序综合使用 locale 和 facet,输出了美式英语和英式英语中的货币符号。注意,根据环境配置,英国货币符号可能显示为问号或方框,或什么都不显示。如果环境能够处理这些符号,那么可得到英镑符号:

//locale locUSEng("en-US"); //For Windows
locale locUSEng("en_US");//For Linux
locale locBritEng("en_GB");//For Linux
//locale locBritEng("en-GB");//For Windowswstring dollars = use_facet<moneypunct<wchar_t>>(locUSEng).curr_symbol();
wstring pounds = use_facet<moneypunct<wchar_t>>(locBritEng).curr_symbol();wcount <<L"In the US,the currency symbol is "<<dollars<<endl;
wcount <<L"In Great Britain, the currency symbol is "<<pounds<<endl;

”正则表达式

正则表达式在头文件中定义,是标准库中的一个强大工具。正则表达式是一种用于字符串处理的微型语言。尽管一开始看上去比较复杂,但一旦了解这种语言,字符串的处理就会简单得多。正则表达式适用于一些与字符串相关的操作

验证: 检查输入字符串是否格式正确。例如: 输入字符串是不是格式正确的电话号码?

决策:判断输入表示哪种字符串。例如: 输入字符串表示 JPEG 文件名还是 PNG 文件名?

解析:, 从输入字符串中提取信息。例如: 从完整的文件名中,提取出不带完整路径和扩展名的文件名

转换,搜索子字符串,并将子字符串蔡换为新的格式化的子字符串。例如: 搜索所有的“C++17”,并替换为“C++”。

遍历,搜索所有的子字符串。例如,从输入字符串中提取所有电话号码。

符号化: 根据一组分隔符将字符串分解为多个子字符串。例如: 根据空白字符、有逗号和句号等将字符串分割为独立的单词。

当然,还可自己编写代码,对字符串执行上述任何操作,但是强烈建议使用正则表达式特性,因为编写正确且安全代码来处理字符串并不容易。在深入介绍正则表达式的细节之前,需要介绍一些重要的术语。下面的术语贯穿于后面的讨论。

模式(pattern): 正则表达式实际上是通过字符串表示的模式。

匹配(match): 判断给定的正则表达式和给定序列[first, last)中的所有字符是否匹配。

搜索(search): 判断在给定序列[first last)中是否存在匹配给定正则表达式的子字符串。

替换(replace): 在给定序列中识别子字符串, 然后将子字符串替换为从其他模式计算得到的新子字符串,其他模式称为替换模式(substitution pattern)。

在网上搜索一下,会发现有好几种不同的正则表达式语法。因此,C++包含对以下几种语法的支持。

ECMAScript: 基于 ECMAScript 标准的语法ECMAScript 是符合 ECMA-262 标准的脚本语言。

JavaScript、ActionScript 和 Jscript 等语言的核心都使用 ECMAScript 语言标准。basic: 基本的 POSIX 语法。extended: 扩展的 POSIX 语法。 awk: POSIX awk 实用工具使用的语法。grep: POSIX grep 实用工具使用的语法。egrep: POSIX grep 实用工具使用的语法,包含-E 参数。如果已经了解了其中任何一种正则表达式语法,就可在 C++中立即使用这种语法,只需要告诉正则表达式库使用那种语法(syntax_option_type)。C++中的默认语法是 ECMAScript,19.2.1 节将详细讲解这种语法。这也是最强大的正则表达式语法,因此强烈建议使用 ECMAScript,而不要使用其他功能受限的语法。本书由于受篇幅限制,不再讲解其他正则表达式语法。

ECMAScript 语法

正则表达式模式是一个字符序列,这种模式表达了要匹配的内容。正则表达式中的任何字符都表示匹配自己,但以下特殊字符除外

^ $ \ . * + ? ( ) [ ] { } |

下面将逐一讲解这些特殊字符。如果需要匹配这些特殊字符,那么需要通过\字符将其转义,例如:

\[ 或\. 或 \* 或\
  1. 锚点

特殊字符和$称为锚点(anchor)。字符匹配行终止符前面的位置,$字符匹配行终止符所在的位置。和$默认还分别匹配字符串的开头和结尾位置,但可以禁用这种行为。例如,test$只匹配字符串 test,不匹配包含 test 和其他任何字符的字符串,例如 ltest、test2 和 testabc 等。

  1. 通配符

通配符(wildcard)可用于匹配除换行符外的任意字符。例如,正则表达式 a.c 可以匹配 abc 和 asc,但不匹配ab5c 和 ac。

  1. 替代

|字符表示“或”的关系。例如,alb 表示匹配 a 或b。

  1. 分组

圆括号0用于标记子表达式,子表达式也称为捕捉组(capture group)。捕捉组有以下用途:

e 捕捉组可用于识别源字符串中单独的子序列,在结果中会返回每一个标记的子表达式(捕提组)。以如下正则表达式为例;,(.)(ablcd)(.)。其中有 3 个标记的子表达式。对字符串 lcd4 运行 regex_search(),执行这个正则表达式会得到含有 4 个条目的匹配结果。第一个条目是完整匹配 lcd4,接下来的 3 个条目是 3 个标记的子表达式。这 3 个条目为: 1、cd 和 4。19.2.3 节会详细讲解如何使用 regex_search()算法。

e 捕捉组可在匹配的过程中用于后向引用(back reference)的目的(后面解释)。

e 捕捉组可在蔡换操作的过程中用于识别组件(后面解释)。

  1. 重复

使用以下 4个重复字符可重复匹配正则表达式中的部分模式:

*匹配零次或多次之前的部分。例如: a*b 可匹配 b、ab、aab 和 aaaab 等字符串。

+匹配一次或多次之前的部分。例如: a+b 可匹配 ab、aab 和 aaaab 等字符串,但不能匹配 b。

?匹配零次或一次之前的部分。例如: a?b 匹配b 和 ab,不能匹配其他任何字符串。

{…}表示区间重复。a{n}重复匹配 a 正好n 次, a{n,}重复将 a 匹配 n 次或更多次, a{n,m}重复将 a 匹配n 到m 次,包含n次和m 次。例如,^a{3,4}引可以匹配 aaa 和 aaaa,但不能匹配 a、aa 和 aaaaa 等字

以上列表中列出的重复匹配字符称为贪禁匹配,因为这些字符可以找出最长匹配,但仍匹配正则表达式的其余部分。为进行非贪禁匹配,可在重复字符的后面加上一个?,例如*?、+?、??和{…}?。非贪禁匹配将其模式重复尽可能少的次数,但仍匹配正则表达式的其余部分。例如,表 19-4 列出了贪禁匹配和非贪禁匹配的正则表达式,以及在输入序列 aaabbb 上运行它们后得到的子字符串。

正则表达式 匹配的子字符串
贪婪匹配:(a+)(ab)*(b+) “aaa” " " “bbb”
非贪婪匹配:(a+?)(ab)*(b+) “aa” “ab” “bb”
  1. 优先级

与数学公式一样,正则表达式中元素的优先级也很重要。正则表达式的优先级如下。

元素: 例如a,是正则表达式最基本的构建块。

量词: 例如+、*、?和{…},紧密绑定至左侧的元素,例如 b+。

串联:例如 ab+c,在量词之后绑定。

替代符: 例如|,最后绑定。

例如正则表达式 ab+cld,它匹配 abc、abbc 和 abbbc 等字符串,还能匹配 d。圆括号可以改变优先级顺序。例如,ab+(cld)可以匹配 abc、abbc、abbbc、.…、abd、abbd 和 abbbd 等字符串。不过,如果使用了圆括号,也意味着将圆括号内的内容标记为子表达式或捕捉组。使用(?..), 可以在避免创建新捕捉组的情况下修改优先级。例如,ab+(?:cld)和之前的 ab+(cld)匹配的内容是一样的,但没有创建多余的捕捉组。

  1. 字符集合匹配

(a|b|c|.…)思这种表达式既元长,又会引入捕捉组,为了避免这种正则表达式,可以使用一种特殊的语法,指定一组字符或字符的范围。此外,还可以使用“和否定”形式的匹配。在方括号之间指定字符集合,[c1c2…cn]可以匹配字符 c1、c2、.…、cn中的任意字符。例如,[abc]可以匹配a、b 和ec 中的任意字符。如果第一个字符是^,那么表示“除了这些字符之外的任意字符

ab[cde]匹配 abc、abd 和 abe。

ab[^cde]匹配 abf 和 abp 等字符串,但不匹配abc、abd 和 abe。

如果需要匹配^、[或]字符本身,需要转义这些字符,例如: [\[^\]匹配[, ^或]。

如果想要指定所有字母,可编写下面这样的字符集合: [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ],但这种写法非常长,这样的模式出现多次的话,看上去会很不优雅,甚至有可能出现拼写错误或不小心漏掉一个字母。这个问题有两种解决方案。一种方案是使用方括号内的范围描述,这人允许使用[a-zA-Z]这样的表达方式,这种表达方式能识别 a 到 z和A到Z范围内的所有字母。如果需要匹配连字符,则需要转义这个字符,例如[a-zA-Z-]+匹配任意单词,包括带连字符的单词。另一种方案是使用某种字符类(character class)。字符类表示特定类型的字符,表示方法为[:name:],可使用什么字符类取决于 locale,但表 19-5 中的名称总是可以识别的。这些字符类的含义也取决于 locale。这个表假定使用标准的 C locale。

字符类别名称 说明
digit 数字
d 同 digit
xdigit 数字和下列十六进制数字使用的字母: ‘a‘、‘b‘、‘e‘、‘d‘、‘e‘、‘f‘、‘A‘、B’、‘C‘、D’、E’、F’
alpha 字母数字字符。对于 C locale,这些是所有的小写和大写字母
alnum alpha 类和 digit 类的组合
w 同 alnum
lower 小写字母(假定适用于 locale)
upper 大写字母(假定适用于 locale)
blank 空白字符是在一行文本中用于分割单词的空格符,对于 C locale,就是’ 或
space 空白字符,对于 C locale,就是"、’\t’、’\n’、’\r’、’\v’和’\f’
s 同space
print 可打印字符。它们占用打印位置,例如在显示器上。与控制符(cntrl)相反,示例有小写字母、大写字母、数字、标点符号字符和空白字符
cntrl 控制符,与可打印字符(print)相反,不占用打印位置,例如在显示器上。对于 C locale,示例有换页符\f、换行符\n和回车符\r等
graph 带有图形表示的字符,包括除空格"'外的所有可打印字符(print)
punct 标点符号字符。对于 C locale,包括不是字母数字(alnum)的所有图形字符(graph),例如’!’ 、"#"、’@’、}等

字符类用在字符集中,例如,英语中的[[:alpha:]]*等同于[a-zA-Z]#。由于有些概念使用非常频繁, 例如匹配数字, 因此这些字符类有缩写模式。 例如, [:digit:]和[:d:]等同于[0-9]。有些类甚至有更短的使用转义符号\的模式。例如,\d 表示[:digit]。因此,通过以下任意模式可以识别一个或多个数字序列

[0-9]+

[[:digit:]]+

[[:d:]]+

\d+

表 19-6 列出了字符类可用的转义符号。

下面举一些示例:

Test[5-8]匹配 Test5、Test6、Test7 和 Test8 。

[[:lower:]]匹配a 和b 等,但不匹配A和了B 等。

[1]匹配除了小写字母(例如 a 和b 等)之外的任意字符。

[[:lower:]5-7]匹配任意小写字母,例如a 和b 等,还匹配数字5、6 和 7。

词边界

词边界(word boundary)的意思可能是: 如果源字符串的第一个字符在单词字符(即字母、数字或下划线)之后,则表示源字符串的开头位置。对于标准的 C locale,这等于[A-Za-z0-9 ]。匹配源字符串的开头位置默认为启用,但也可以禁用(regex_constants::match_not_ bow)。

如果源字符串的最后一个字符是单词字符之一,则表示源字符串的结束位置。匹配源字符串的结束位置默认为启用,但也可以禁用(regex_constants::match_not_eow)。

单词的第一个字符,这个字符是单词字符之一,而且之前的字符不是单词字符。

单词的结尾字符,这是单词字符之后的非单词字符,之前的字符是单词字符。通过\b 可匹配单词边界,通过\B 匹配除单词边界外的任何内容。

  1. 后向引用

通过后向引用(back reference)可引用正则表达式本身的捕捉组: \n 表示第n 个捕提组,且 n>0。例如,正则表达式(\d+)-.*-\1 匹配以下格式的字符串:

在一个捕捉组中(d+)捕提的一个或多个数字 接下来是一个连字符- 接下来是 0 个或多个字符.*接下来是另一个连字符- 接下来是第一个捕捉组捕捉到的相同数字\1

这个正则表达式能匹配 123-abc-123 和 1234-a-1234 等字符串,但不能匹配 123-abc-1234 和 123-abc-321 等

  1. lookahead

    正则表达式支持正向 lookahead(?=模式)和负向 lookahead(?!模式)。lookahead 后面的字符必须匹配(正向)或不匹配(负向)lookahead 模式,但这些字符还没有使用。例如,a(?!b)模式包含一个负向 lookahead,以匹配后面不跟b 的字母。a(?=b)模式包含一个正向 lookahead,以匹配后跟b 的字母,但不使用b,b 不是匹配的一部分。下面是一个更复杂的示例。正则表达式匹配一个输入序列,该输入序列至少包含一个小写字母、至少一个大写字母、至少一个标点符号,并且至少 8 个字符长。例如,可使用下面这样的正则表达式来强制密码满足特定条件。

    (?=.*[[:Lower:]])(?=.*[[:upper:]])(?=.*[[:punct:]]) .{8,}

  2. 正则表达式和原始字符串字面量

从前面的讨论可以看出, 正则表达式经常使用很多应该在普通 C++字符串字面量中转义的特殊字符。例如,如果在正则表达式中写一个d,这个\d 能匹配任何数字。然而,由也是 C++中的一个特殊字符,因此需要在正则表达式的字符串字面量中将其转义为\\d,和否则 C++编译器会试图将其解释为\d。如果需要正则表达式匹配单个反斜杠\\,那么会更加麻烦。因\是正则表达式语法本身的一个特殊字符,所以应该将其转义为,最终得到\\\\

使用原始字符串字面量可使 C++源代码中的复杂正则表达式更容易阅读。第 2 章讲解了原始字符串字面量。例如以下正则表达式,

"(|\\n|\\r|\\\\)"

这个正则表达式搜索空格、换行符、回车符和反斜本。从中可看出,这个正则表达式需要使用很多转义字符。使用原始字符串字面量,这个正则表达式可蔡换为以下更便于阅读的版本;

R"(( |\n|\r|\\))"

原始字符串字面量以 R"(开头,以)"结束。开头和结尾之间的所有内容都是正则表达式。当然,在最后还需要双反斜枉,因为反斜杠在正则表达式本身中需要转义。以上就是对 ECMAScript 语法的简单介绍。下面开始讲解如何在 C++代码中真正使用正则表达式。

19.2.2 regex 库

正则表达式库的所有内容都在头文件和 std 名称空间中。正则表达式库中定义的基本模板类型包括如下几种。

basic_regex: 表示某个特定正则表达式的对象。

match_results: 匹配正则表达式的子字符串,包括所有的捕捉组。它是 sub_match 的集合。

sub_match: 包含输入序列中一个迭代器对的对象,这些迭代器表示匹配的特定捕捉组。和迭代器对中的-个迭代器指向匹配的捕提组中的第一个字符,另一个欠代器指向匹配的捕捉组中最后一个字符后面的那个字符。它的 str()方法把匹配的捕捉组返回为字符串。

regex 库提供了 3 个关键算法: regex_match()、regex_search()和 regex_replace()。所有这些算法都有不同的版本,不同的版本允许将源字符串指定为 STL 字符串、字符数组或表示开始和结束的欠代器对。和迭代器可以有具

➤ ➤ const char*
➤ ➤ const wchar_t*
➤ ➤ string::const_iterator
➤ ➤ wstring::const_iterator

事实上,可使用任何具有双向迭代器行为的迭代器。第 17 章和第 18 章更深入地讨论了迭代器。regex 库还定义了以下两类正则表达式迭代器, 这两类正则表达式迭代器非常适合于查找源字符串中的所有模式。

regex_iterator: 遍历一个模式在源字符串中出现的所有位置。

regex_token_iterator: 遍历一个模式在源字符串中出现的所有捕捉组。为方便 regex 库的使用,C++标准定义了很多属于以上模板的类型别名,如下所示;

using regex = basic_regex<char>;
using wregex = basic_regex<wchar_t>;
using csub_match = sub_match<const char*>;
using wcsub_match = sub_match<const wchar_t*>;
using ssub_match = sub_match<string::const_iterator>;
using wssub_match = sub_match<wstring::const_iterator>;
using cmatch = match_results<const char*>;
using wcmatch = match_results<const wchar_t*>;
using smatch = match_results<string::const_iterator>;
using wsmatch = match_results<wstring::const_iterator>;
using cregex_iterator = regex_iterator<const char*>;
using wcregex_iterator = regex_iterator<const wchar_t*>;
using sregex_iterator = regex_iterator<string::const_iterator>;
using wsregex_iterator = regex_iterator<wstring::const_iterator>;
using cregex_token_iterator = regex_token_iterator<const char*>;
using wcregex_token_iterator = regex_token_iterator<const wchar_t*>;
using sregex_token_iterator = regex_token_iterator<string::const_iterator>;
using wsregex_token_iterator = regex_token_iterator<wstring::const_iterator>;

下面将讲解 regex_match()、regex_search0和 regex_replace()算法以及 regex_iterator 和 regex_token _iterator 类。

regex_match()

regex_match()算法可用于比较给定的源字符串和正则表达式模式。 如果正则表达式模式匹配整个源字符串,则返回 true,和否则返回false。这个算法很容易使用。regex_match()算法有 6 个版本,这些版本接收不同类型的参数。它们都使用如下形式:

template<...>
bool regex_match(InputSequence[, MatchResults], RegEx[, Flags]);

InputSequence 可以表示为:

➤ ➤ 源字符串的首尾迭代器
➤ ➤ std::string
➤ ➤ C 风格的字符串

可选的 MatchResults 参数是对 match_results 的引用,它接收匹配。如果 regex_match()返回false,就只能调用 match_results::empty()或 match_results::size(),其余内容都未定义。如果 regex_match()返回 true,表示找到匹配,可以通过 match_results 对象查看匹配的具体内容。具体方法稍后用示例说明。RegEx 参数是需要匹配的正则表达式。可选的 Flags 参数指定匹配算法的选项。大多数情况下,可使用默认选项。更多细节可参阅标准库参考资料,见附录 B。

regex_match()示例

假设要编写一个程序,要求用户输入采用以下格式的日期: 年/月/日,其中年是4 位数,月是 1到 12 之间的数字(包括 1 和 12), 日是 1 到 31 之间的数字(包括 1 和 31)。通过正则表达式和 regex_match()算法可以验证用户的输入,如下所示:

regex r("\\d{4}/(?:0?[1-9]|1[0-2])/(?:0?[1-9]|[1-2][0-9]|3[0-1])");
while (true)
{cout << "Enter a date (year/month/day) (q=quit): ";string str;if (!getline(cin, str) || str == "q")break;if (regex_match(str, r))cout << " Valid date." << endl;elsecout << " Invalid date!" << endl;
}

第一行创建了一个正则表达式,它由 3 部分组成,这 3 部分通过斜杠字符/隔开,分别表示年、月、日。下面解释这 3 部分。

\\d{4}: 这部分匹配任意 4 位数的组合,例如 1234 和 2010 等。

(?:0?[1-9]|1[0-2]) :: 正则表达式的这一部分包括在括号中,从而确保正确的优先级。这里不需要使用任何捕提组,所以使用了(?…)。内部的表达式由|字符分隔的两部分组成。

​ 0?[1-9]: 匹配 1到9之间的任何数字(包括 1 和 9),前面有一个可选的 0。例如,可以匹配 1、2、9、03 和 04 等。不匹配 0、10 和 11 等。

​ 1[0-2]: 只能匹配 10、11 和 12,不能匹配除此之外的其他任何字符串。

(?:0?[1-9]|[1-2][0-9]|3[0-1]) :这一部分也包括在非捕捉组中,由 3 个可选的部分组成。0?[1-9]: 匹配 1到9之间的任何数字(包括 1 和 9),前面有一个可选的0。例如,可以匹配 1、2、9、03 和 04 等。不匹配0、10 和 11 等。 ,

​ [1-2][0-9]: 匹配 10 和 29 之间的任何数字(包括 10 和 29),不能匹配除此之外的其他任何字符串。

3[0-1]: 只能匹配 30 和 31,不能匹配除此之外的其他任何字符串。

这个例子然后进入一个无限循环, 要求用户输入一个日期.将接下来输入的每一个日期都传入regex_match()算法。当 regex_match()返回 true 时,表示用户输入的日期匹配正则表达式的日期模式。这个例子可稍作扩充,要求 regex_match()算法在结果对象中返回捕捉到的子表达式。为理解这段代码,首先要理解捕捉组的作用。通过指定 match_results 对象,例如调用 regex_match()时指定的 smatch,正则表达式匹配字符串时会将 match_results 对象中的元素填入。为提取这些子字符串,必须使用括号创建捕捉组。match_results 对象中的第一个元素[0]包含匹配整个模式的字符串。在使用 regex_match()且找到匹配时,这就是整个源序列。在使用 19.2.4 节讲解的 regex_search()时,这表示源序列中匹配正则表达式的一个子字符串。元素[1]是第一个捕捉组匹配的子字符串,[2]是第二个捕提组匹配的子字符串,依此类推。为获得捕捉组的字符串表示,可像下面的代码这样编写 m[i]或 m[i].str(),其中是捕提组的索引,m 是 match_results 对象。

如下代码将年、月、日提取到三个独立的整型变量中。修改后的例子中的正则表达式有一些微小变化。匹配年的第一部分被放在捕捉组中, 匹配月和日的部分现在也在捕捉组中, 而不在非捕捉组中。 调用 regex_match()时提供了 smatch 参数,现在这个参数会包含匹配的捕提组。下面是修改后的示例,

#include <vector>
#include <algorithm>
#include <iostream>
#include <numeric>
#include <regex>
using namespace std;int main(int argc, char *argv[])
{regex r("(\\d{4})/(0?[1-9]|1[0-2])/(0?[1-9]|[1-2][0-9]|3[0-1])");while (true) {cout << "Enter a date (year/month/day) (q=quit): ";string str;if (!getline(cin, str) || str == "q")break;smatch m;if (regex_match(str, m, r)) {int year = stoi(m[1]);int month = stoi(m[2]);int day = stoi(m[3]);cout << " Valid date: Year=" << year<< ", month=" << month<< ", day=" << day << endl;} else {cout << " Invalid date!" << endl;}}return 0;
}

在这个例子中,smatch 结果对象中有 4 个元素。

[0]:匹配整个正则表达式的字符串,在这个例子中就是完整的日期

[1]:年

[2]:月

[3]:日

执行这个例子,可得到以下输出:

xz@xiaqiu:~/study/test/test$ ./test
Enter a date (year/month/day) (q=quit): 2021/10/1Valid date: Year=2021, month=10, day=1
Enter a date (year/month/day) (q=quit): 21/10/1Invalid date!
Enter a date (year/month/day) (q=quit):

注意

这个日期匹配示例只检查日期是否由年(4 位数)、月(1~ 12)、日(1~31)组成,但没有对是否为头年、月份中的天数是否正确等进行验证。如果需要执行这些验证,还必须编写代码,对 regex_match()提取出来的年、月、日进行验证。如果在代码中验证年、 月、日,那么可以简化正则表达式:

regex r("(\\d{4})/(\\d{1,2})/(\\d{1,2})");

regex_search()

如果整个源字符串匹配正则表达式,那么前面介绍的 regex_match()算法返回 true,否则返回 false。这个算法不能用于查找源字符串中匹配的子字符串,但通过 regex_search()算法可以在源字符串中搜索匹配特定模式的子字符串。regex_search()算法有 6 个不同版本。它们都具有如下形式:

template<...>
bool regex_search(InputSequence[, MatchResults], RegEx[, Flags]);

在输入字符串中找到匹配时,所有变体返回 true,和否则返回 但false 参数类似于 regex_match()的参数。有两个版本的 regex_search()算法接收要处理的字符串的首尾和迭代器。你可能想在循环中使用 regex_search()的这个版本,通过操作每个 regex_search()调用的首尾迭代器,找到源字符串中某个模式的所有实例。千万不要这样做! 如果正则表达式中使用了锚点(^或$)和单词边界等,这样的程序会出问题。由于空匹配,这样会产生无限循环。根据本章后面讲解的内容,使用 regex_iterator 或 regex_token_iterator 在源字符串中提取出某个模式的所有实例。

警告:

绝对不要在循环中通过 regex_search()在源字符串中搜索一个模式的所有实例。要改用 regex_iterator 或regex_token_iterator。

regex_search()示例

regex_search()算法可在输入序列中提取匹配的子字符串。下例从输入的代码行中提取代码注释。正则表达式搜索的子字符串以//开头,然后跟上一些可选的空白字符\\s*,之后是一个或多个在捕提组中捕捉的字符(+)。这个捕提组只能捕捉注释子字符串。smatch 对象 m 将收到搜索结果。如果成功,m[1]包含找到的注释。可检查m[l].first 和 m[1].second 迭代器,以确定注释在源字符串中的准确位置。

regex r("//\\s*(.+)$");
while (true)
{cout << "Enter a string with optional code comments (q=quit): ";string str;if (!getline(cin, str) || str == "q")break;smatch m;if (regex_search(str, m, r))cout << " Found comment '" << m[1] << "'" << endl;elsecout << " No comment found!" << endl;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a string with optional code comments (q=quit): string str; //commitFound comment 'commit'
Enter a string with optional code comments (q=quit): test;//注释Found comment '注释'
Enter a string with optional code comments (q=quit): //注释Found comment '注释'
Enter a string with optional code comments (q=quit): //No comment found!
Enter a string with optional code comments (q=quit): //dssaFound comment 'dssa'
Enter a string with optional code comments (q=quit):

regex_iterator

根据前面的解释,绝对不要在循环中通过 regex_search()获得模式在源字符串中的所有实例。应改用regex_iterator 或 regex_token_iterator。这两个迭代器和标准库容器的迭代器类似。

regex_iterator 示例

下面的例子要求用户输入源字符串,然后从源字符串中提取出所有的单词,最后将单词打印在引号之间。这个例子中的正则表达式为[w]+,以搜索一个或多个单词字母。这个例子使用 std::string 作为来源,所以使用sregex_iterator 作为迭代器。这里使用了标准的迭代器循环,但是在这个例子中,尾迭代器的处理和普通标准库容器的尾迭代器稍有不同。一般情况下,需要为某个特定的容器指定尾从代器,但对于 regex_iterator,只有一个end 迭代器。只需要通过默认的构造函数声明 regex_iterator 类型,就可获得这个尾迭代器。for 循环创建了一个迭代器 iter,它接收源字符串的首尾迭代器以及正则表达式作为参数。每次找到匹配时调用循环体,在这个例子中是每个单词。sregex_iterator 遍历所有的匹配。通过解引用 sregex_iterator,可得到一个 smatch 对象。访问这个 smatch 对象的第一个元素[0]可得到匹配的子字符串:

regex reg("[\\w]+");
while(true)
{cout<<"Enter a string to split (q = quit): ";string str;if(!getline(cin,str) || str == "q")break;const sregex_iterator end;for(sregex_iterator iter(cbegin(str),cend(str),reg);iter != end; ++iter){cout<<"\""<<(*iter)[0]<<"\""<<endl;}
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a string to split (q = quit): this a test
"this"
"a"
"test"
Enter a string to split (q = quit):

从这个例子中可以看出,即使是简单的正则表达式,也能执行强大的字符串操作。注意,regex_iterator 和 regex_token_iterator 在内部都包含一个指向给定正则表达式的指针。它们都显式删除接收右值正则表达式的构造函数,因此无法使用临时 regex 对象构建它们。例如,下面的代码无法编译:

for(sregex_iterator iter(cbegin(str),cend(str),regex("[\\w]+"));iter != end;++iter){...}

regex_token_iterator

19.2.5 节讲解了 regex_iterator,这个迭代器遍历每个匹配的模式。在循环的每次迭代中都得到一个match_results 对象,通过这个对象可提取出捕提组捕捉的那个匹配的子表达式。regex_token_iterator 可用于在所有匹配的模式中自动遍历所有的或选中的捕捉组。regex_token_iterator 有 4个构造函数,格式如下:

regex_token_iterator(BidirectionalIterator a,
BidirectionalIterator b,
const regex_type& re
[, SubMatches
[, Flags]]);

所有构造函数都需要把首尾迭代器作为输入序列, 还需要一个正则表达式。可选的 SubMatches 参数用于指定应迭代哪个捕捉组。可以用 4 种方式指定 SubMatches:

➤➤ 一个整数,表示要迭代的捕捉组的索引。

➤➤一个vector,其中的整数表示要迭代的捕捉组的索引。

➤➤带有捕捉组索引的 initializer_list。

➤➤带有捕捉组索引的 C 风格数组。

忽略 SubMatches 或把它指定为 0 时, 获得的迭代器将遍历索引为 0 的所有捕捉组, 这些捕捉组是匹配整个正则表达式的子字符串。可选的 Flags 参数指定匹配算法的选项。大多数情况下,可以使用默认选项。更多细循环体中使用*iter 而非(*iter[0]),因为使用 submatch 的默认值 0 时,记号迭代器会自动遍历索引为 0 的所有捕循环体中使用*iter 而非(*iter[0]),因为使用 submatch 的默认值 0 时,记号迭代器会自动遍历索引为 0 的所有捕捉组。这段代码的输出和 regex_iterator 示例完全一致,

regex reg("[\\w]+");
while(true)
{cout<<"Enter a string to split (q = quit):";string str;if(!getline(cin,str) || str == "q")break;const sregex_token_iterator end;for(sregex_token_iterator iter(cbegin(str),cend(str),reg);iter != end;++iter){cout<<"\""<<*iter<<"\""<<endl;}
}

下面的示例要求用户输入一个日期, 然后通过 regex_token_iterator 遍历第二个和第三个捕捉组(月和日), 这是通过整数 vector 指定的。本章已经解释了用于日期的正则表达式。唯一的区别是添加了^和$锚点,以匹配整个源序列。前面的示例不需要它们,因为使用了 regex_match(),这会自动匹配整个输入字符串。

#include <iostream>
#include <string>
#include <regex>using namespace std;int main()
{regex reg("^(\\d{4})/(0?[1-9]|1[0-2])/(0?[1-9]|[1-2][0-9]|3[0-1])$");while(true){cout<<"Enter a date(year/month/day)(q = quit):";string str;if(!getline(cin,str) || str == "q")break;vector<int> indices{2,3};const sregex_token_iterator end;for(sregex_token_iterator iter(cbegin(str),cend(str),reg,indices);iter != end;++iter){cout<<"\""<<*iter<<"\""<<endl;}}return 0;
}

这段代码只打印合法日期的月和日。这个例子的输出如下所示:

xz@xiaqiu:~/study/test/test$ ./test
Enter a date(year/month/day)(q = quit):2021/10/1
"10"
"1"
Enter a date(year/month/day)(q = quit):2021/10/43
Enter a date(year/month/day)(q = quit):2021/1/1
"1"
"1"
Enter a date(year/month/day)(q = quit):

regex_token_iterator 还可用于执行字段分解(field splitting)或标记化(tokenization)这样的任务。使用这种方法比使用 C 语言中的旧式 strtok()函数更加灵活和安全。 标记化是在 regex_token_iterator 构造函数中通过将要遍历的捕捉组索引指定为 - 1 触发的。在标记化模式中,和迭代器会遍历源字符串中不匹配正则表达式的所有子字符串。下面的代码演示了这个过程,这段代码根据前后带有任意数量的空白字符的分隔符,和;对一个字符串进行标记化操作

regex reg(R"(\s*[,;]\s*)");
while(true)
{cout<<"Enter a string to split on ',' and ';' (q = quit):";string str;if(!getline(cin,str) || str == "q")break;const sregex_token_iterator end;for(sregex_token_iterator iter(cbegin(str),cend(str),reg,-1);iter!=end;++iter){cout<<"\""<<*iter<<"\""<<endl;}
}

这个例子中的正则表达式被指定为源字符串字面量,搜索匹配以下内容的模式:

1.0个或多个空白字符

2.后面跟着,或;字符

3.后面跟着 0 个或多个空白字符

xz@xiaqiu:~/study/test/test$ ./test
Enter a string to split on ',' and ';' (q = quit):this is a test
"this is a test"
Enter a string to split on ',' and ';' (q = quit):this;is;s;test
"this"
"is"
"s"
"test"
Enter a string to split on ',' and ';' (q = quit):this;is a;test
"this"
"is a"
"test"
Enter a string to split on ',' and ';' (q = quit):thi s; is a;tes t
"thi s"
"is a"
"tes t"
Enter a string to split on ',' and ';' (q = quit):

regex_replace()

regex_replace()算法要求输入一个正则表达式,以及一个用于替换匹配子字符串的格式化字符串。这个格式化字符串可通过表 19-7 中的转义序列,引用匹配子字符串中的部分内容。

转义序列 转换为
$n 匹配第n 个捕捉组的字符串,例如1 表示第一个捕捉组,$2 表示第二个捕捉组,依此类推,n 必须大于0
$& 匹配整个正则表达式的字符哩
$’ 在输入序列中,在匹配正则表达式的子字符串左侧的部分
$’ 在输入序列中,在匹配正则表达式的子字符串右侧的部分
$$ 单个美元符号

regex_replace()算法有 6 个不同版本。 这些版本之间的区别在于参数的类型。其中的 4 个版本使用如下格式:

string regex_replace(InputSequence, RegEx, FormatString[, Flags]);

这 4 个版本都在执行替换操作后返回得到的字符串。InputSequence 和 FormatString 可以是 std::string 或 C风格的字符串。RegEx 参数是需要匹配的正则表达式。可选的 Flags 参数指定替换算法的选项。regex_replace()算法的另外两个版本采用如下形式;

OutputIterator regex_replace(OutputIterator,
BidirectionalIterator first,
BidirectionalIterator last,
RegEx, FormatString[, Flags]);

这两个版本把得到的字符串写入给定的输出迭代器, 并返回这个输出欠代器。 输入序列给定为首尾迭代器。其他参数与 regex_replace()的另外 4 个版本相同。

regex_replace()示例

第一个例子的源 HTML 字符串是Header

Some text

,正则表达式为(.*)

(.*)

。表 19-8 展示了不同的转义序列以及替换后的文字。

转义序列 转换为
$1 Header
$2 Some text
$&

Header

Some text

$’
$’

下面的代码演示了 regex_replace()的使用:

const string str("<body><h1>Header</h1><p>Some text</p></body>");
regex r("<h1>(.*)</h1><p>(.*)</p>");
const string format("H1=$1 and P=$2"); // See above table
string result = regex_replace(str, r, format);
cout << "Original string: '" << str << "'" << endl;
cout << "New string: '" << result << "'" << endl;

输出

xz@xiaqiu:~/study/test/test$ ./test
Original string: '<body><h1>Header</h1><p>Some text</p></body>'
New string: '<body>H1=Header and P=Some text</body>'
xz@xiaqiu:~/study/test/test$

另一个例子是接收一个输入字符串,然后将每个单词边界转换为一个换行符,使目标字符串在每一行只包含一个单词。下面的例子演示了这一点,但没有使用任何循环来处理给定的输入字符串。这段代码首先创建一个匹配单个单词的正则表达式。当发现匹配时,匹配字符串被蔡换为$1\n,其中$1 将被匹配的单词蔡代。还要注意,这里使用了 format_no_copy 标志以避免将空白字符和其他非单词字符从源字符串复制到输出。

regex reg("([\\w]+)");
const string format("$1\n");
while(true)
{cout<<"Enter a string to split over multiple lines (q = quit):";string str;if(!getline(cin,str) || str == "q")break;cout<<regex_replace(str,reg,format,regex_constants::format_no_copy)<<endl;
}

输出

xz@xiaqiu:~/study/test/test$ ./test
Enter a string to split over multiple lines (q = quit):this is a test
this
is
a
test
Enter a string to split over multiple lines (q = quit):

  1. :lower: ↩︎

c++高级编程学习笔记5相关推荐

  1. 【C#8.0 and .NET Core 3.0 高级编程学习笔记】

    @C#8.0 and .NET Core 3.0 高级编程学习笔记 前言 为了能精细地完成对C#语言的学习,我决定选择一本书,精读它,理解它,记录它.我想选择什么书并不是最重要的,最重要的是持之以恒的 ...

  2. Windows高级编程学习笔记(一)

    写在前面的话 之前学的Windows编程都是界面啊.网络编程啊之类的纯应用层面的东西,总是感觉而自己没有达到自己期望中的水平.什么水平呢?如果让你编写监控系统资源的工具,或者DLL注入相关软件,或者底 ...

  3. 高级编程学习笔记day01(知识点篇)

    文件IO学习笔记 1. 文件描述符:所有打开的文件都通过文件描述符引用.     文件描述符0与进程的标准输入关联     文件描述符1与进程的标准输出关联     文件描述符2与进程的标准错误关联 ...

  4. javascript高级编程学习笔记(二)——继承

    2019独角兽企业重金招聘Python工程师标准>>> 写读书笔记的好处在于加深记忆,前一篇总结了编程中创建的对象的几种方式,以及常用的方式,这一篇总结实现继承的方式: 1.对象冒充 ...

  5. c++高级编程学习笔记4

    C++运算符重载 运算符重载概述 根据第 1 章的描述,C++中的运算符是一些类似于+.<.*和<<的符号.这些运算符可应用于内建类型,例如 int 和 double,从而实现算术操 ...

  6. Windows高级编程学习笔记(二)

    第三章 进程 发现这本书的文字很简练,知识点突出,而且翻译的基本没有拗口的地方,是本好书,(^o^)/~ 下面进入正题. 关于内存映射 Windows内存管理的分页机制在微机原理课程中有提到,后面的章 ...

  7. Unix环境高级编程学习笔记(七) 多线程

    线程概述 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程(process ...

  8. Unix环境高级编程学习笔记(一)

    第二章 文件I/O 1.文件描述符   对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数.   Unix shell使用文件描述符0表示标准输入,1表示标准输出,2表示标准出 ...

  9. Windows高级编程学习笔记(三)

    第四章 线程 知识要点 每向系统获取一个句柄,会使相应对象的引用计数加1.而GetCurrentProcess()函数返回的是一个伪句柄,也就是不增加引用计数,相当于赋值拷贝.对应于线程,有GetCu ...

最新文章

  1. 网站排名在首位后,为什么还要继续做SEO?
  2. python画图哆啦a梦-【Python】绘制哆啦A梦
  3. 广域网应用场景包括哪些?—Vecloud
  4. BZOJ2815: [ZJOI2012]灾难
  5. 使用 requests 配置代理服务
  6. UI5 libraries determined in Backend
  7. 字符串分割与存入List集合
  8. python缺少标准库_干货分享:Python如何自动导入缺失的库
  9. 假如你心中有个莎乐美
  10. Python中的函数(调用、参数、返回值、变量的作用域)
  11. 《Web漏洞防护》读书笔记——第2章,SQL注入防护
  12. sd卡、U盘作为启动盘后容量变小处理方法
  13. javascript animation lib greensock gsap介绍
  14. jQuery实现选择“学科门类”、“学科大类(一级学科)”、“专业”(二级学科)实现三级联动
  15. MQ如何快速实现流量削峰填谷
  16. 南方都市报:红心照耀MSN
  17. SGE集群主机和执行机的正确卸载
  18. manjaro双屏显示
  19. Java源码HashMap、ConcurrentHashMap:JDK1.8HashMap静态常量以及设置的目的,初始容量、最大容量、扩容缩容树化条件
  20. java的环境变量如何设置

热门文章

  1. MIXLAB_NASA_TICKET生成
  2. 计算机无法安装蓝牙设备,笔记本蓝牙无法添加设备解决方法
  3. 三个常见博弈游戏以及 SG 函数和 SG 定理
  4. iOS逆向工程-工具篇
  5. 安装任何版本ActiveSync都出错原因
  6. Excel数据透视表经典教程七《刷新及更改数据源》
  7. 锤子android 7,锤子正式加入安卓7.1.1阵容 一加3/3T尝鲜氢OS公测版
  8. 使用QPST刷机时报ERROR: function: main:314 Could not connect to \\.\COM3
  9. 键盘的复制粘贴快捷键总是需要重复多次才起作用
  10. 机器学习数据挖掘-软件、网站、课程资源知识点汇总