如何为用户定义的类型专门化std :: hash <t>?</t>

时间:2014-06-23 08:53:25

标签: c++ c++11 unordered-map unordered-set hash-function

问题

对于用户定义类型的std :: unordered_map或std :: unordered_set的第三个模板参数,std :: hash有什么好的专业化,所有成员数据类型都具有良好的std :: ::散列?

对于这个问题,我将“好”定义为易于实现和理解,合理有效且不太可能产生哈希表冲突。商品的定义不包括任何有关安全性的陈述。

什么是Google可用

目前,两个StackOverflow问题是谷歌搜索“标准哈希专业化”的第一个点击。

第一个How to specialize std::hash::operator() for user-defined type in unordered containers?解决了打开std命名空间和添加模板特化是否合法的问题。

第二个How to specialize std::hash for type from other library,基本上解决了同样的问题。

这留下了当前的问题。鉴于C ++标准库的实现为标准库中的基本类型和类型定义了散列函数,为用户定义的类型专门化std :: hash的简单有效方法是什么?有没有一种很好的方法来组合标准库实现提供的哈希函数?

(编辑感谢dyp。)StackOverflow上的Another question解决了如何组合哈希函数的

其他Google搜索结果不再有用。

This Dobbs博士的文章指出,两个令人满意的哈希的XOR将产生一个新的令人满意的哈希值。

This文章似乎是从知识中说出来并暗示了许多事情,但却注重细节。它在第一个例子的简短评论中与Dobbs博士的文章相矛盾,称使用XOR组合散列函数会产生一个弱的结果散列函数。

因为XOR应用于任何两个相等的值导致0,我可以看出为什么XOR本身很弱。

元问题

一个很好的理由回答解释为什么这个问题无效且一般无法回答也是受欢迎的。

4 个答案:

答案 0 :(得分:6)

一种简单的方法是使用boost::hash库和extend it for your type。它有一个很好的扩展函数hash_combinestd::hash缺少),可以轻松组合结构中各个数据成员的哈希值。

换句话说:

  1. 为您自己的类型重载boost::hash_value
  2. 为您自己的类型专门化std::hash并使用boost::hash_value实现它。
  3. 通过这种方式,您可以获得最好的标准和提升世界,std::hash<>boost::hash<>适用于您的类型。


    更好的方法是在N3980 Types Don't Know #中使用建议的新散列基础架构。此基础结构使hash_combine不必要。

答案 1 :(得分:3)

首先,Dobbs博士的文章说两个的XOR 令人满意的哈希会产生令人满意的哈希简单 错误。这是贫穷哈希的好方法。一般来说,到 创建一个好的哈希,首先将对象分解为 子对象,每个子对象都存在良好的哈希值 结合哈希。这样做的一个简单方法就是这样 喜欢:

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 if 你有一系列的价值观。)

当然,这应该认为所有的亚型都有好处 std::hash的实现。对于基础类型和 字符串,这是给定的;对于您自己的类型,只需应用 上面的规则是递归的,专门用std::hash来使用 关于其子类型的HashAccumulator。对于标准容器 一个基本类型,它有点棘手,因为你不是(正式的, 至少)允许在类型上专门化标准模板 来自标准库;你可能不得不创造 直接和明确使用HashAccumulator的类 指定如果你需要这样一个容器的哈希值。

答案 2 :(得分:2)

在我们获得标准库以帮助解决此问题之前:

  1. 下载现代的幽灵,如SpookyHash:http://burtleburtle.net/bob/hash/spooky.html
  2. std::hash<YourType>的定义中,创建一个SpookyHash实例,并Init。请注意,在流程启动或std::hash构造中选择随机数,并将其用作初始化make it slightly harder to DoS your program, but doesn't fix the problem
  3. 获取结构中与operator==(&#34;突出字段&#34;)相关的每个字段,并将其输入SpookyHash::Update
    • 请注意类似double的类型:他们有char[]的2个表示形式,可比较==-0.00.0。还要注意具有填充的类型。在大多数机器上,int没有,但很难判断struct是否会。 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html#is_contiguously_hashable对此进行了讨论。
    • 如果你有子结构,你可以通过递归地将它们的字段输入到同一个SpookyHash实例中来获得更快,更高质量的哈希值。但是,这需要为这些结构添加方法或手动提取显着字段:如果您不能这样做,只需将其std::hash<>值提供给顶级{{}}即可接受。 1}}实例。
  4. SpookyHash
  5. 返回SpookyHash::Final的输出

答案 3 :(得分:1)

您的操作is required

  • 返回size_t
  • 类型的值
  • ==运营商保持一致。
  • 对于不相等的值,哈希冲突的可能性很小。

没有明确要求哈希值均匀分布在size_t整数范围内。 cppreference.com notes

  

[标准库]的一些实现使用将整数映射到自身的普通(标识)散列函数

避免哈希冲突加上这个弱点意味着你的类型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的哈希值也是如此。因此,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范围内传播哈希码值,那将丢弃信息(由于溢出)

使用乘法加法和加法的方法累计组件哈希码值,如typically done in 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;
       }
    };
 }