C++中如何对自定义类型做hash操作
C++中如何对自定义类型做hash操作
问题描述
出现在使用没有定义作为 unordered_map
的键的 pair
类型或 vector
类型的hash键值。
unordered_map<pair<int, int>, int> mp; // pair没有hash值
unordered_map
中用 std::hash
来计算 key
,但是C++中没有给 pair
做Hash的函数,所以不能用 pair
作为 unordered_map
的key。同理,unordered_set
这类也是哈希表的也不能用 pair
类型或 vector
类型等其他类型。
但是!map
可以!map
里面是通过 operator<
来比较大小,而 pair
是可以比较大小的。所以,map
是可以用 pair
做键的。
上述代码,编译器不知道如何将pair
通过什么哈希函数完成映射,因此报错:
error: call to implicitly-deleted default constructor of 'std::__1::hash<std::__1::pair<int, int> >'
: _Hash() {}
原理
哈希函数(hash function)的目的是根据给定对象算出一个哈希码(hash code),使得对象经过hash code映射之后能够乱且随机地被放置在哈希表(hashtable)中,从而尽量避免发生哈希碰撞。
C++标准库中提供地hash函数版本如下(使用偏特化,对于数值型地数据hash函数得到地hash code就是原值,对于字符串则提供了专门的hash表达式):
来自侯捷老师的C++STL源码分析
template <typename key> struct hash {}; // 泛化
// 针对内置类型的偏特化
template <> struct hash<char> { size_t operator()(char x) const { return x; } };
template <> struct hash<short> { size_t operator()(shortx) const { return x; } };
template <> struct hash<int> { size_t operator()(int x) const { return x; } };
// ......
template <> struct hash<long> { size_t operator()(longx) const { return x; } };
// ......
template <> struct hash<char *> { size_t operator()(char *s) const { return _stl_hash_string(s); } };
size_t _stl_hash_string(const char *s) {
unsigned long h = 0;
for (; *s; ++s)
h = 5 * h + *s;
return size_t(s);
}
容器类的哈希函数
在C++的STL中,unordered_set
、unordered_multiset
、unordered_map
、unordered_multimap
的底层数据结构就是hash表。以 unordered_set
为例,作为模板类具体形式如下:
unordered_set<typename _Kty, typename _Hasher = hash<_Kty>
, typename _Keyeq = equal_to<_Kty>, typename _Allocator<_Kty>>
- 第一个模板参数为向unordered_set安插的对象类型
- 第二个模板参数为hash函数
- 第三个参数对象为两个对象相等的判别条件(这里需要说明的是对于set、map容器,需要有一个机制来专门判断安插的两个对象是否相等,然后决定是否插入以及插入位置)
- 第四个模板参数为分配器allocator
对于我们常见的数据类型如int、double、char、char*、string等,STL已经帮忙写好了hash函数,因此可以直接使用。对于自定义类型,需要提供哈希函数。
解决方案
需要实现自定义哈希函数。如前所述,容器类的后三个对象都是可调用对象,具体来说这里是仿函数。
实现自定义hash的功能,有2个要求:
- 自定义哈希函数必须是可调用对象,实现哈希的算法(将自定义数据转为哈希码)
- 为自定义类型提供判断两对象是否相等的功能,有两种方式:
- 在类中重载
operator==
- 创建一个实现对象比较功能的仿函数并作为模板参数传递给unordered_set。
- 在类中重载
struct Person {
Person() = default;
Person(string fn, string ln, int a) : firstname(fn), lastname(ln), age(a) {}
// 重载==运算符
bool operator==(const Person &p) const
{
return (firstname == p.firstname) && (lastname == p.lastname) && (age == p.age);
}
string firstname;
string lastname;
int age;
friend class Hasher;
};
法1 使用仿函数
struct Hasher { // hash函数对象,得到hash码
size_t operator()(const Person &p) const
{
// 使用了std::hash
return hash<string>()(p.firstname) + hash<string>()(p.lastname) + hash<int>()(p.age);
}
};
// 安装hash函数,传递仿函数
unordered_set<Person, Hasher> uset;
法2 使用函数指针
size_t hasher(const Person &p)
{ // hash函数,得到hash码
return hash<string>()(p.firstname) + hash<string>()(p.lastname) + hash<int>()(p.age);
}
// 安装hash函数,传递函数指针
unordered_set<Person, decltype(&hasher)> uset(3, hasher);
法3 使用标准库模板偏特化
因为C++标准库已经提供了hash的类模板,所以可以直接对自定义类型做一个偏特化版本。这种方法使用时不需要将仿函数以第二个模板参数的形式传递给容器。
template<>
struct hash<Person> {//偏特化(这里使用了标准库已经提供的hash偏特化类hash<string>,hash<int>())
size_t operator()(const Person& p) const {
return hash<string>()(p.firstname)+ hash<string>()(p.lastname)+ hash<int>()(p.age);
}
};
法4 自行实现Hash哈数
这里先给出为 pair
使用的一般情况下的哈希函数,可能需要具体情况自行更改。
struct hashfunc { // 封装在结构体里,相当于重载了operator()的类,因此这里安装的时候只需要填入类型名hashfunc即可
template<typename T, typename U>
size_t operator() (const pair<T, U> &i) const {
return hash<T>()(i.first) ^ hash<U>()(i.second);
}
};
另外,还可以利用可变参数模板来实现任意个数的参数组合进行哈希的仿函数:
template <typename T>
void hashCombine(size_t &seed, const T &arg) //真正的hash在这里完成
{
//这里虽然也用到了标准库提供的hash函数,但是后面可以添加自己的一些数据(甚至hash<T>()(arg)操作也可以有我们自己来做)
//不同用户在这里可以有不同的数,只要能够将原始数据尽可能打乱即可
// 0x9e3779b9涉及到数学中的黄金比例,实际上并不需要一定是这个数
seed ^= hash<T>()(arg) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template <typename T>
void hashValue(size_t &seed, const T &arg) //③递归出口
{
hashCombine(seed, arg);
}
template <typename T1, typename... T2>
void hashValue(size_t &seed,
const T1 &arg,
const T2 &...args) //②在这里展开一个模板形参,通过递归+引用逐步拿到所有参数,当args...的大小为1时跳出该递归,接着进入③
{
hashCombine(seed, arg);
hashValue(seed, args...); //递归
}
template <typename... T> // T为模板形参包,可以代表任意多个类型;args为函数形参包,可以代表任意多个函数参数
size_t hashValue(const T &...args) //①在这里完成参数的第一次拆分,接着进入②
{
size_t seed = 0; //种子,以引用方式传递
hashValue(seed, args...); // args...中为T类型对象中的所有用于hash的数据成员
return seed;
}
// 最终的Hasher仿函数
struct Hasher {
size_t operator()(const Person &p) const { return hashValue(p.firstname, p.lastname, p.age); }
};