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

How to specialize std::hash<T> for user defined types?

本文关键字:专门化 类型 std hash 用户 lt gt 定义      更新时间:2023-10-16

问题

对于所有成员数据类型都有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缺乏),允许轻松组合您的结构的单个数据成员的哈希值。

换句话说:

  1. 为自己的类型重载boost::hash_value
  2. 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的类如果需要这样的容器的散列,请指定

直到我们在标准中得到一个库来帮助解决这个问题:

  1. 下载一个现代哈希器,如SpookyHash: http://burtleburtle.net/bob/hash/spooky.html。
  2. std::hash<YourType>的定义中,创建一个SpookyHash实例,并创建Init实例。请注意,在进程启动或std::hash构建时选择一个随机数,并使用它作为初始化将使DoS程序稍微困难一些,但不能解决问题。
  3. 取结构中对operator==有贡献的每个字段("显著字段"),并将其输入SpookyHash::Update
      注意像double这样的类型:它们有两种表示形式作为char[]来比较==: -0.00.0。还要注意有填充的类型。在大多数机器上,int不支持,但是很难判断struct是否支持。http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html#is_contiguously_hashable讨论了这个
  4. 如果你有子结构,你会得到一个更快、更高质量的哈希值,递归地将它们的字段输入到相同的SpookyHash实例中。然而,这需要为这些结构添加一个方法或手动提取显著字段:如果你不能这样做,可以将它们的std::hash<>值提供给顶级SpookyHash实例。
  5. 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;
       }
    };
 }