为啥我不能用一对作为键来编译 unordered_map?

Posted

技术标签:

【中文标题】为啥我不能用一对作为键来编译 unordered_map?【英文标题】:Why can't I compile an unordered_map with a pair as key?为什么我不能用一对作为键来编译 unordered_map? 【发布时间】:2015-09-20 23:56:04 【问题描述】:

我正在尝试创建一个 unordered_map 来映射整数对:

#include <unordered_map>

using namespace std;
using Vote = pair<string, string>;
using Unordered_map = unordered_map<Vote, int>;

我有一个班级,我已将 Unordered_map 声明为私有成员。

但是,当我尝试编译它时出现以下错误:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/type_traits:948:38: 未定义模板'std::__1::hash, std: 的隐式实例化: :__1::basic_string > >'

如果我使用像 map&lt;pair&lt;string, string&gt;, int&gt; 这样的常规地图而不是 unordered_map,我不会收到此错误。

在无序映射中不能使用pair 作为键吗?

【问题讨论】:

【参考方案1】:

您需要为您的密钥类型提供合适的哈希函数。一个简单的例子:

#include <unordered_map>
#include <functional>
#include <string>
#include <utility>

// Only for pairs of std::hash-able types for simplicity.
// You can of course template this struct to allow other hash functions
struct pair_hash 
    template <class T1, class T2>
    std::size_t operator () (const std::pair<T1,T2> &p) const 
        auto h1 = std::hash<T1>(p.first);
        auto h2 = std::hash<T2>(p.second);

        // Mainly for demonstration purposes, i.e. works but is overly simple
        // In the real world, use sth. like boost.hash_combine
        return h1 ^ h2;  
    
;

using Vote = std::pair<std::string, std::string>;
using Unordered_map = std::unordered_map<Vote, int, pair_hash>;

int main() 
    Unordered_map um;

这可行,但没有最好的哈希属性。在组合哈希时,您可能希望查看boost.hash_combine 之类的内容以获得更高质量的结果。在this answer 中也更详细地讨论了这一点——包括上述来自 boost 的解决方案。

对于实际使用:Boost 还提供了函数集 hash_value,它已经为 std::pairstd::tuple 和大多数标准容器提供了哈希函数。


更准确地说,它会产生太多的碰撞。例如,每个对称对将散列为 0,仅通过排列不同的对将具有相同的散列。这可能适合您的编程练习,但可能会严重影响实际代码的性能。

【讨论】:

您引用了hash_value,但链接指向hash。我认为hash 是正确的位置,因为hash_value 的文档建议使用hash。我以为我会让你编辑而不是自己做...... @PeterVermont hash_value 记录在我链接的页面上。你有更好的链接吗? @Baum mit Augean 链接是正确的,但我建议链接的文本说“hash”而不是“hash_value” 小心:“仅通过排列不同的对将具有相同的哈希”。说得更直白一点:pair_hash(a, b) ==pair_hash(b, a) 很少是你想要的! (罪魁祸首是 XOR。return h1 ^(h2 &lt;&lt; 1); 应该修复它。) @Unapiedra 稍微好一点,但仍然不是很好;例如permuted 的哈希值仍然相当接近。这个网站上有关于如何正确操作的问题。【参考方案2】:

我首选的解决这个问题的方法是定义一个key 函数,将你的对转换为一个唯一的整数(或任何可散列的数据类型)。该键不是散列键。它是这对数据的唯一 ID,然后将由 unordered_map 进行最佳散列。例如,你想定义一个unordered_map 的类型

  unordered_map<pair<int,int>,double> Map;

而你想使用Map[make_pair(i,j)]=valueMap.find(make_pair(i,j))对地图进行操作。然后你必须告诉系统如何散列一对整数make_pair(i,j)。取而代之的是,我们可以定义

  inline size_t key(int i,int j) return (size_t) i << 32 | (unsigned int) j;

然后将地图的类型改为

  unordered_map<size_t,double> Map;

我们现在可以使用Map[key(i,j)]=valueMap.find(key(i,j)) 对地图进行操作。现在每个make_pair 都变成调用内联key 函数。

这种方法保证了key会被最优地散列,因为现在散列部分是由系统完成的,系统总是选择内部散列表大小为素数,以确保每个桶的可能性相同。但是您必须让自己 100% 确定 key 对于每对都是唯一的,即,没有两个不同的对可以具有相同的密钥,否则可能很难找到错误。

【讨论】:

祝你好运,为pair&lt;string,string&gt; 做 OP 希望。 即使 OP 想为pair&lt;string,string&gt; 这样做——这对于pair&lt;int,int&gt; 来说仍然是一个好方法,因为key 是双射的。可以帮助别人! @Zhuoran He 我只是想确认一下,这个内联函数只适用于64位系统对吧?据我了解,这在 32 位系统上对于一对整数来说会失败,对吧? 由于 size_t 不能保证是 64 位的,您应该使用 uint64_t 代替。【参考方案3】:

如果使用pair不是严格要求,你可以简单地使用两次map。

#include <unordered_map>

using namespace std;
using Unordered_map = unordered_map<string, unordered_map<string, int>>;

Unordered_map um;
um["Region1"]["Candidate1"] = 10;
cout << um["Region1"]["Candidate1"];    // 10

【讨论】:

这里如何编写基于范围的for循环 这将需要两次哈希表查找,而不仅仅是一次。因此,如果表被频繁访问,它将显着降低性能,例如如果用作某种缓存。另一方面,可读性要好得多! 解决方案简单合理,哈希函数很可能用于每个哈希表查找。虽然选择不当或实施不当的自定义哈希函数对性能的影响可能比双重查找更糟糕。【参考方案4】:

对于pair key,我们可以使用boost pair hash function:

#include <iostream>
#include <boost/functional/hash.hpp>
#include <unordered_map>
using namespace std;

int main() 
  unordered_map<pair<string, string>, int, boost::hash<pair<string, string>>> m;

  m[make_pair("123", "456")] = 1;
  cout << m[make_pair("123", "456")] << endl;
  return 0;

同样我们可以对向量使用 boost hash,

#include <iostream>
#include <boost/functional/hash.hpp>
#include <unordered_map>
#include <vector>
using namespace std;

int main() 
  unordered_map<vector<string>, int, boost::hash<vector<string>>> m;
  vector<string> a("123", "456");

  m[a] = 1;
  cout << m[a] << endl;
  return 0;

【讨论】:

很遗憾,boost::hash 似乎不适用于元组。【参考方案5】:

正如您的编译错误所示,您的 std 命名空间中没有 std::hash&lt;std::pair&lt;std::string, std::string&gt;&gt; 的有效实例化。

根据我的编译器:

错误 C2338 C++ 标准没有为此提供哈希 类型。 c:\程序文件(x86)\微软视觉工作室 14.0\vc\include\xstddef 381

您可以为std::hash&lt;Vote&gt; 提供自己的专业化,如下所示:

#include <string>
#include <unordered_map>
#include <functional>

using namespace std;
using Vote = pair<string, string>;
using Unordered_map = unordered_map<Vote, int>;

namespace std

    template<>
    struct hash<Vote>
    
        size_t operator()(Vote const& v) const
        
            // ... hash function here ...
        
    ;


int main()

    Unordered_map m;

【讨论】:

我想你的意思是const Vote&amp; v @GuyKogus "const Vote&" 和 "Vote const&" 完全一样。***.com/questions/5503352/const-before-or-const-after 我认为this is Undefined Behavior:只有当声明依赖于用户定义的类型并且特化满足原始的所有要求时,才允许将任何标准库模板的模板特化添加到命名空间std模板。【参考方案6】:

参考:C++ Standard Library: A tutorial and reference, Second version 第 7.9.2 章:创建和控制无序容器

我在 Google 中找到的所有解决方案都使用 XOR 来生成 pair 的哈希码,这非常糟糕。见why-is-xor-the-default-way-to-combine-hashes。然而,这本书给了我们最好的解决方案,使用hash_combine,取自Boost。当我在 Online Judge(Atcoder) 中测试时,该解决方案比 XOR 好得多。我将代码组织为模板,如下所示。您可以尽可能多地复制和粘贴它。并且可以很方便地更改它以适应任何自定义结构/类。

更新:为元组添加哈希模板。

#include <functional>

namespace hash_tuple 
template <typename TT> struct hash 
    size_t operator()(TT const &tt) const  return std::hash<TT>()(tt); 
;

// from boost (functional/hash):
// see http://www.boost.org/doc/libs/1_35_0/doc/html/hash/combine.html template
template <class T> inline void hash_combine(std::size_t &seed, T const &v) 
    seed ^= hash_tuple::hash<T>()(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);


// Recursive template code derived from Matthieu M.
template <class Tuple, size_t Index = std::tuple_size<Tuple>::value - 1>
struct HashValueImpl 
    void operator()(size_t &seed, Tuple const &tuple) const 
        HashValueImpl<Tuple, Index - 1>(seed, tuple);
        hash_combine(seed, std::get<Index>(tuple));
    
;
template <class Tuple> struct HashValueImpl<Tuple, 0> 
    void operator()(size_t &seed, Tuple const &tuple) const 
        hash_combine(seed, std::get<0>(tuple));
    
;

template <typename... TT> struct hash<std::tuple<TT...>> 
    size_t operator()(std::tuple<TT...> const &tt) const 
        size_t seed = 0;
        HashValueImpl<std::tuple<TT...>>(seed, tt);
        return seed;
    
;
// auxiliary generic functions to create a hash value using a seed
template <typename T> inline void hash_val(std::size_t &seed, const T &val) 
    hash_combine(seed, val);


template <typename T, typename... Types>
inline void hash_val(std::size_t &seed, const T &val, const Types &... args) 
    hash_combine(seed, val);
    hash_val(seed, args...);


template <typename... Types>
inline std::size_t hash_val(const Types &... args) 
    std::size_t seed = 0;
    hash_val(seed, args...);
    return seed;


struct pair_hash 
    template <class T1, class T2>
    std::size_t operator()(const std::pair<T1, T2> &p) const 
        return hash_val(p.first, p.second);
    
;
 // namespace hash_tuple

#include <bits/stdc++.h>

int main() 
    using ll = long long;
    // std::unordered_map<std::pair<ll, ll>, ll, hash_tuple::pair_hash>
    // hashmapPair; std::unordered_set<std::pair<ll, ll>, hash_tuple::pair_hash>
    // hashsetPair;

    std::unordered_map<std::pair<ll, ll>, ll, hash_tuple::pair_hash>
        hashmapPair;
    hashmapPair[0, 0] = 10;
    std::unordered_set<std::pair<ll, ll>, hash_tuple::pair_hash> hashsetPair;
    hashsetPair.insert(1, 1);

    using TI = std::tuple<ll, ll, ll, ll>;
    std::unordered_map<TI, ll, hash_tuple::hash<TI>> hashmapTuple;
    hashmapTuple[0, 1, 2, 3] = 10;
    std::unordered_set<TI, hash_tuple::hash<TI>> hashsetTuple;
    hashsetTuple.emplace(0, 1, 2, 3);

    return 0;


【讨论】:

【参考方案7】:

Baum mit Augen 的 answer 上的 cmets 中,用户 Joe Black asked for an example 使用 lambda expressions 而不是定义哈希函数。我同意 Baum mit Augen 的 opinion,认为这可能会损害可读性,尤其是如果您想实现更通用的解决方案。因此,我想通过关注 OP 提出的 std::pair&lt;std::string, std::string&gt; 的特定解决方案来简化我的示例。该示例还使用了handcrafted 组合std::hash&lt;std::string&gt; 函数调用:

using Vote = std::pair<std::string, std::string>;
auto hash = [](const Vote& v)
    return std::hash<std::string>()(v.first) * 31 + std::hash<std::string>()(v.second);
;
using Unordered_map = std::unordered_map<Vote, int, decltype(hash)>;
Unordered_map um(8, hash);

Code on Ideone

【讨论】:

【参考方案8】:

对于这些问题有一个hack

使用std:unordered_map 中的string

看下面的例子-

我需要散列一个矩形的端点(角)

错误方法

unordered_map<pair<int, int>, int> M;           //ERROR

pair<int, int> p;
M[p]++;

破解

unordered_map<string, int> M;

pair<int, int> p;
string s = to_string(p.first) + "_" + to_string(p.second);
M[s]++;

如果您需要创建十进制散列或双倍作为密钥,这种黑客甚至可以工作:)

【讨论】:

以上是关于为啥我不能用一对作为键来编译 unordered_map?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能增加 std::unordered_map 迭代器?

我的网页为啥不能用键盘的上下键来翻动滚动条了啊

为啥我不能更改 unordered_map 返回的对象的成员变量?

为啥 std::unordered_set 不将 CComBSTR 类型作为键?

为啥这段代码不能用 VS2010 和 gcc 4.8.1 编译

为啥我编译的MFC应用程序在其他电脑上不能运行?