如何为<T>用户定义的类型专门化 std::hash?
How to specialize std::hash<T> for user defined types?
问题
对于所有成员数据类型都有std::hash的良好特化的用户定义类型,在std::unordered_map或std::unordered_set的第三个模板形参中使用std::hash的良好特化是什么?对于这个问题,我将"好"定义为易于实现和理解,相当高效,并且不太可能产生哈希表冲突。好的定义不包括任何关于安全的陈述。
Google'able的状态
目前,两个StackOverflow问题是Google搜索"std散列专门化"的第一个结果。
第一,如何对无序容器中用户定义类型的std::hash::operator()进行专门化?,处理打开STD命名空间和添加模板专门化是否合法。
第二个,如何专门化std::hash for type from other library,本质上解决了同样的问题。
这就留下了当前的问题。考虑到c++标准库的实现为基本类型和标准库中的类型定义了哈希函数,那么为用户定义的类型特化std::哈希的简单而有效的方法是什么?是否有一种好方法来组合由标准库实现提供的哈希函数?
(编辑感谢dyp。)StackOverflow上的另一个问题是如何组合一个对哈希函数。
其他的Google结果没有更多的帮助。
Dr. Dobbs的这篇文章指出,两个满意哈希的异或将产生一个新的满意哈希。
这篇文章似乎是从知识出发的,暗示了很多东西,但细节却很少。它在第一个例子中的简短评论中与Dr. Dobbs的文章相矛盾,说使用异或组合哈希函数会产生一个弱的哈希函数。
因为对任意两个相等的值进行异或运算的结果是0,所以我可以理解为什么异或运算本身是弱的。
Meta Question
一个合理的答案解释为什么这个问题是无效的,不能被普遍回答也将是受欢迎的。
一个简单的方法是使用boost::hash
库并为您的类型扩展它。它有一个很好的扩展函数hash_combine
(std::hash
缺乏),允许轻松组合您的结构的单个数据成员的哈希值。
换句话说:
- 为自己的类型重载
boost::hash_value
- 将
std::hash
专门化为您自己的类型,并使用boost::hash_value
实现它。
这样你就得到了std和boost的最佳效果,std::hash<>
和boost::hash<>
都适合你的类型。
一个更好的方法是使用N3980 Types Don't Know #中提出的新哈希基础结构。这种基础结构使得hash_combine
变得不必要。
首先是Dobbs博士的文章,文章中说到两个的异或令人满意的哈希会产生令人满意的哈希很简单错了。这是处理劣质散列的好方法。一般来说,创建一个好的散列,首先将对象分解为子对象,每个子对象都有一个好的散列,和组合哈希。一个简单的方法是如:
class HashAccumulator
{
size_t myValue;
public:
HashAccumulator() : myValue( 2166136261U ) {}
template <typename T>
HashAccumulator& operator+=( T const& nextValue )
{
myValue = 127U * myValue + std::hash<T>( nextHashValue );
}
HashAccumulator operator+( T const& nextHashValue ) const
{
HashAccumulator results( *this );
results += nextHashValue;
return results;
}
};
(这已经被设计,以便您可以使用std::accumulate
如果你有一个值序列。)
当然,这是假设所有的亚型都有良好的std::hash
的实现。对于基本类型和字符串,这是给定的;对于你自己的类型,只需应用上面的规则递归地,专门化std::hash
来使用HashAccumulator
的子类型。的标准容器一个基本类型,它有点棘手,因为你不是(正式地,(至少)允许对一个类型的标准模板进行专门化从标准库;你可能需要创造一个直接显式使用HashAccumulator
的类如果需要这样的容器的散列,请指定
直到我们在标准中得到一个库来帮助解决这个问题:
- 下载一个现代哈希器,如SpookyHash: http://burtleburtle.net/bob/hash/spooky.html。
- 在
std::hash<YourType>
的定义中,创建一个SpookyHash
实例,并创建Init
实例。请注意,在进程启动或std::hash
构建时选择一个随机数,并使用它作为初始化将使DoS程序稍微困难一些,但不能解决问题。 - 取结构中对
operator==
有贡献的每个字段("显著字段"),并将其输入SpookyHash::Update
。- 注意像
double
这样的类型:它们有两种表示形式作为char[]
来比较==
:-0.0
和0.0
。还要注意有填充的类型。在大多数机器上,int
不支持,但是很难判断struct
是否支持。http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html#is_contiguously_hashable讨论了这个 - 如果你有子结构,你会得到一个更快、更高质量的哈希值,递归地将它们的字段输入到相同的
SpookyHash
实例中。然而,这需要为这些结构添加一个方法或手动提取显著字段:如果你不能这样做,可以将它们的std::hash<>
值提供给顶级SpookyHash
实例。 - 从
std::hash<YourType>
返回SpookyHash::Final
的输出
您的操作需要
- 返回
size_t
类型的值 - 与
==
操作符一致 - 对于不相等的值有一个低概率的哈希冲突。
没有明确要求哈希值在size_t
整数范围内均匀分布。cppreference.com
指出
标准库的一些实现使用平凡的(identity)哈希函数将整数映射到自身
避免哈希冲突加上这个弱点意味着您的类型的std::hash
专门化应该永远不要简单地使用(快速)按位异或(^
)来组合数据成员的子哈希。考虑这个例子:
struct Point {
uint8_t x;
uint8_t y;
};
namespace std {
template<>
struct hash< Point > {
size_t operator()(const Point &p) const {
return hash< uint8_t >(p.x) ^ hash< uint8_t >(p.y);
}
};
}
p.x
的哈希值在[0,255]范围内,p.y
的哈希值也在[0,255]范围内。因此,Point
的哈希值也将在[0,255]范围内,有256(=2^8)个可能的值。有256*256(=2^16)个唯一的Point
对象(std::size_t
通常支持2^32或2^64值)。因此,对于好的散列函数,哈希冲突的概率应该近似于2^(-16)。我们的函数给出了哈希碰撞的概率小于2^(-8)这很糟糕:我们的哈希只提供了8位信息,但是一个好的哈希应该提供16位信息。
如果你的数据成员的哈希函数只提供std::size_t
范围内较低部分的哈希值,你必须在组合它们之前"移位"组件哈希的位,这样它们每个都提供独立的信息位。左移看起来很简单
return (hash< uint8_t >(p.x) << 8) ^ hash< uint8_t >(p.y);
,但如果hash< uint8_t >
的实现(在本例中)试图将哈希码值扩展到std::size_t
范围内,则将丢弃信息(由于溢出)。
使用乘素数加方法累积组件哈希码值,通常在Java中这样做,通常效果更好:
namespace std {
template<>
struct hash< Point > {
size_t operator()(const Point &p) const {
const size_t prime = 257;
size_t h {hash< uint8_t >(p.x)};
h = h * prime + hash< uint8_t >(p.y);
return h;
}
};
}
- 线程 std::调用未知类型,无法专门化函数错误
- 如何为所有非数组类型专门化模板?
- 如何在同一个模板功能上专门化几种类型?
- 如何为底层类型 int 的枚举专门化类
- 扣除指南的尾随回报类型不是专门化
- C++模板方法专门化联合类型
- 专门化模板数据类型的模板
- 无法专门化 std::hash 以unordered_map存储自定义类型
- 使用非类型模板参数进行专门化模板模板参数
- g++和clang++在结构/类专门化中具有非类型参数的不同行为
- C++模板专门化除一种类型外的所有类型
- 基于类型特征专门化强制转换运算符
- 使用decltype尾部返回类型专门化函数模板
- 可以转发声明的类型模板参与模板专门化
- 专门化泛型类型的函数组
- 错误:T没有命名类型-用于使用强类型枚举的专门化
- 模板类在模板类型之间转换,但也专门化
- 是否可以为模板化类型专门化模板
- 如何在任意依赖类型上专门化模板
- 如何根据模板参数是否具有别名来专门化类型