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

Posted

技术标签:

【中文标题】如何为用户定义的类型专门化 std::hash<T>?【英文标题】:How to specialize std::hash<T> for user defined types? 【发布时间】:2014-08-13 06:06:56 【问题描述】:

问题

什么是 std::hash 的良好特化,可用于 std::unordered_map 或 std::unordered_set 的第三个模板参数,用于所有成员数据类型都已具有良好特化 std:: 的用户定义类型哈希?

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

Google 的现状

目前,有两个 *** 问题是 Google 搜索“std hash specialization”的首选。

第一个,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 编辑。)*** 上的Another question 解决了如何组合散列函数的

其他 Google 搜索结果没有更多帮助。

ThisDobbs 博士的文章指出,两个令人满意的哈希值的异或将产生一个新的令人满意的哈希值。

This的文章似乎是从知识中说话,暗示了很多东西,但对细节很轻。它与 Dobbs 博士在第一个示例中的简短评论中的文章相矛盾,即使用 XOR 组合哈希函数会导致结果哈希函数较弱。

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

元问题

一个合理的答案解释为什么这个问题是无效的并且一般不能回答也是受欢迎的。

【问题讨论】:

也许您应该将有关combining hashes 的问题添加到列表中? 嗯,我不确定。该答案适用于 两个 值,我不知道当递归应用于 N 个值时算法的质量是否足够好。似乎即使tuple 也不能使用标准设施进行哈希处理,请参阅***.com/q/7110301 我们正在研究它的标准,但现在它很棘手。 open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html 有一个很好的方法,但它使编译器更难优化。希望我们能够在接下来的 6 个月内解决这个问题(抱歉,标准很慢),并在下一个实验版本中加入一些东西。 这里有一个 open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html 的公共域部分实现:github.com/HowardHinnant/hash_append/blob/master/hash_append.h 和大量使用它的示例代码:github.com/HowardHinnant/hash_append 你不能完全实现它,这就是为什么它必须进行标准化。但是,我现在在现实世界的项目中使用它已经足够好了。它消除了组合步骤,并允许您选择并轻松切换所使用的哈希算法,即使对于原始类型也是如此。 Bloomberg 刚刚开源了他们的生产质量 N3980 实施:github.com/bloomberg/bde/blob/master/groups/bsl/bslh/doc/… 【参考方案1】:

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

    下载现代散列器,例如 SpookyHash:http://burtleburtle.net/bob/hash/spooky.html。 在std::hash&lt;YourType&gt;的定义中,创建一个SpookyHash实例,Init它。请注意,在进程启动或std::hash 构造时选择一个随机数,并将其用作初始化将make it slightly harder to DoS your program, but doesn't fix the problem。 获取结构中对operator== 有贡献的每个字段(“显着字段”),并将其输入SpookyHash::Update。 注意像double 这样的类型:它们有2 个表示为char[] 的比较==-0.00.0。还要注意具有填充的类型。在大多数机器上,int 不会,但很难判断struct 是否会。 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html#is_contiguously_hashable 讨论这个问题。 如果您有子结构,您将通过递归地将它们的字段输入到相同的SpookyHash 实例中获得更快、更高质量的哈希值。但是,这需要向这些结构添加一个方法或手动提取显着字段:如果您不能这样做,可以将其 std::hash&lt;&gt; 值输入到*** SpookyHash 实例中。 从std::hash&lt;YourType&gt;返回SpookyHash::Final的输出。

【讨论】:

Bloomberg 已经开源了他们的生产质量 N3980 实现:github.com/bloomberg/bde/blob/master/groups/bsl/bslh/doc/… SpookyHash 实现在这里找到:github.com/bloomberg/bde/blob/master/groups/bsl/bslh/…【参考方案2】:

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

换句话说:

    为您自己的类型重载boost::hash_value。 为您自己的类型专门化 std::hash 并使用 boost::hash_value 实现它。

这样您就可以充分利用 std 和 boost 世界,std::hash&lt;&gt;boost::hash&lt;&gt; 都适合您的类型。


更好的方法是使用N3980 Types Don't Know # 中提议的新哈希基础架构。这种基础架构使hash_combine 变得不必要。

【讨论】:

问题是hash_combine使用的记录算法是一个很差的算法。 @JamesKanze 你可以做出贡献,让它变得更好。 @JamesKanze 我想知道你用什么指标来区分一个糟糕的哈希组合器和一个好的组合器? 数学分析。我不知道有什么方法可以保证一个是好的,但是移位(而不是乘以奇数)意味着初始元素最终会被移出,并且对哈希没有影响。 Boost 的 'hash_combine' 左右移动,并与初始值异或,所以不会丢失初始值。您通常使用code.google.com/p/smhasher/wiki/SMHasher 之类的程序来测试哈希函数。 Boost的功能我没有亲自测试过。【参考方案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 的类 指定是否需要此类容器的哈希。

【讨论】:

查看相关问题的答案,了解为什么 XOR 不是组合哈希的好方法:***.com/a/27952689/545127【参考方案4】:

你的操作is required到

返回一个size_t类型的值 与== 运算符保持一致。 对于不相等的值,哈希冲突的可能性很低。

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

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

避免哈希冲突以及该弱点意味着您的类型的 std::hash 特化应该永远简单地使用(快速)按位 XOR (^) 来组合您的子哈希数据成员。考虑这个例子:

 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 值)。因此,good 散列函数的散列冲突概率应该约为 2^(-16)。我们的函数给出的哈希冲突概率略低于 2^(-8)。这很糟糕:我们的哈希只提供 8 位信息,但一个好的哈希应该提供 16 位信息。

如果您的数据成员的散列函数仅提供 std::size_t 范围的低部分中的散列值,则您必须在组合它们之前“移动”组件散列的位,因此它们每个都提供独立的信息位.左移看起来很简单

       return (hash< uint8_t >(p.x) << 8) ^ hash< uint8_t >(p.y);

但是如果hash&lt; uint8_t &gt; 的实现(在这种情况下)试图将哈希码值分布在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;
       
    ;
 

【讨论】:

以上是关于如何为用户定义的类型专门化 std::hash<T>?的主要内容,如果未能解决你的问题,请参考以下文章

无法对指向成员的指针使用 std::hash 的特化?

使用 Symfony 2 CLI 工具,如何为子类生成具有正确类型提示的 getter 和 setter?

如何为自定义控件创建事件

如何为 ACTION_OPEN_DOCUMENT 意图实现自定义 MIME 类型?

如何为矢量专门化一个类模板?

如何为 Django 模型定义默认数据?