05 | 容器汇编 II:需要函数对象的容器
函数对象及其特化
在讲容器之前,我们需要首先来讨论一下两个重要的函数对象,less 和 hash。
我们先看一下 less,小于关系。在标准库里,通用的 less 大致是这样定义的:
template <class T>
struct less
: binary_function<T, T, bool> { //继承binary_function模板类,具体可参考侯捷老师的教程
bool operator()(const T& x,
const T& y) const
{
return x < y;
}
};
也就是说,less 是一个函数对象,并且是个二元函数,执行对任意类型的值的比较,返回布尔类型。作为函数对象,它定义了函数调用运算符(operator()),并且缺省行为是对指定类型的对象进行 < 的比较操作。
计算哈希值的函数对象 hash 就不一样了。它的目的是把一个某种类型的值转换成一个无符号整数哈希值,类型为 size_t。它没有一个可用的默认实现。
对于常用的类型,系统提供了需要的特化
template <class T> struct hash;
template <>
struct hash<int>
: public unary_function<int, size_t> {
size_t operator()(int v) const
noexcept
{
return static_cast<size_t>(v);
}
};
我们用下面这个例子来加深一下理解:
#include <algorithm> // std::sort
#include <functional> // std::less/greater/hash
#include <iostream> // std::cout/endl
#include <string> // std::string
#include <vector> // std::vector
#include "output_container.h"
using namespace std;
int main()
{
// 初始数组
vector<int> v{13, 6, 4, 11, 29};
cout << v << endl;
// 从小到大排序
sort(v.begin(), v.end());
cout << v << endl;
// 从大到小排序
sort(v.begin(), v.end(),
greater<int>());
cout << v << endl;
cout << hex;
auto hp = hash<int*>(); //c++11新特性 自动推导一个函数对象
cout << "hash(nullptr) = "
<< hp(nullptr) << endl;
cout << "hash(v.data()) = "
<< hp(v.data()) << endl;
cout << "v.data() = "
<< static_cast<void*>(v.data())
<< endl;
auto hs = hash<string>();
cout << "hash(\"hello\") = "
<< hs(string("hello")) << endl;
cout << "hash(\"hellp\") = "
<< hs(string("hellp")) << endl;
}
在 MSVC 下的某次运行结果如下所示:
{ 13, 6, 4, 11, 29 }
{ 4, 6, 11, 13, 29 }
{ 29, 13, 11, 6, 4 }
hash(nullptr) = a8c7f832281a39c5
hash(v.data()) = 7a0bdfd7df0923d2
v.data() = 000001EFFB10EAE0
hash("hello") = a430d84680aabd0b
hash("hellp") = a430e54680aad322
对于容器也是如此,函数对象的类型确定了容器的行为。
priority_queue
priority_queue 也是一个容器适配器。上一讲没有和其他容器适配器一起讲的原因就在于它用到了比较函数对象(默认是 less)。它和 stack 相似,支持 push、pop、top 等有限的操作,但容器内的顺序既不是后进先出,也不是先进先出,而是(部分)排序的结果。在使用缺省的 less 作为其 Compare 模板参数时,最大的数值会出现在容器的“顶部”。如果需要最小的数值出现在容器顶部,则可以传递 greater 作为其 Compare 模板参数。
关联容器
关联容器有 set(集合)、map(映射)、multiset(多重集)和 multimap(多重映射)。跳出 C++ 的语境,map(映射)的更常见的名字是关联数组和字典 [3],而在 JSON 里直接被称为对象(object)。在 C++ 外这些容器常常是无序的;在 C++ 里关联容器则被认为是有序的。
关联容器是一种有序的容器。名字带“multi”的允许键重复,不带的不允许键重复。set 和 multiset 只能用来存放键,而 map 和 multimap 则存放一个个键值对。
与序列容器相比,关联容器没有前、后的概念及相关的成员函数,但同样提供 insert、emplace 等成员函数。此外,关联容器都有 find、lower_bound、upper_bound 等查找函数,结果是一个迭代器
- find(k) 可以找到任何一个等价于查找键 k 的元素(!(x < k || k < x))
- lower_bound(k) 找到第一个不小于查找键 k 的元素(!(x < k))
- upper_bound(k) 找到第一个大于查找键 k 的元素(k < x)
如果你需要在 multimap 里精确查找满足某个键的区间的话,建议使用 equal_range,可以一次性取得上下界(半开半闭)。如下所示:
#include <tuple>
multimap<string, int>::iterator
lower, upper;
std::tie(lower, upper) =
mmp.equal_range("four");
如果在声明关联容器时没有提供比较类型的参数,缺省使用 less 来进行排序。如果键的类型提供了比较算符 < 的重载,我们不需要做任何额外的工作。否则,我们就需要对键类型进行 less 的特化,或者提供一个其他的函数对象类型。
对于自定义类型,我推荐尽量使用标准的 less 实现,通过重载 <(及其他标准比较运算符)对该类型的对象进行排序。存储在关联容器中的键一般应满足严格弱序关系(strict weak ordering;)
- 对于任何该类型的对象 x:!(x < x)(非自反)
- 对于任何该类型的对象 x 和 y:如果 x < y,则 !(y < x)(非对称)
- 对于任何该类型的对象 x、y 和 z:如果 x < y 并且 y < z,则 x < z(传递性)
- 对于任何该类型的对象 x、y 和 z:如果 x 和 y 不可比(!(x < y) 并且 !(y < x))并且 y 和 z 不可比,则 x 和 z 不可比(不可比的传递性)
无序关联容器
这些容器和关联容器非常相似,主要的区别就在于它们是“无序”的。这些容器不要求提供一个排序的函数对象,而要求一个可以计算哈希值的函数对象。你当然可以在声明容器对象时手动提供这样一个函数对象类型,但更常见的情况是,我们使用标准的 hash 函数对象及其特化。