用struct做unordered_map的key

執假以為真發表於2020-11-28

本篇文章著重討論如何在STL的 unordered_map 中以 struct 作為 key. 
unordered_map 是STL中的關聯容器,自然就是一個模板類。其宣告如下:

template < class Key,                                    // unordered_map::key_type
           class T,                                      // unordered_map::mapped_type
           class Hash = hash<Key>,                       // unordered_map::hasher
           class Pred = equal_to<Key>,                   // unordered_map::key_equal
           class Alloc = allocator< pair<const Key,T> > > class unordered_map; 
                                                         // unordered_map::allocator_type

第1個引數是key的型別,第2個引數是value的型別,第5個是記憶體分配模型。關鍵是第3個引數和第4個引數。
第3個引數 Hash 是一個一元的函式物件型別;該函式物件只有一個引數,其型別是雜湊表的 Key 的型別。預設情況下,第3個引數傳入的是 std::hash<key> ,它返回的是一個型別為 size_t 的數字,用途是在雜湊表中定位表項所在。注意,若要自己實現第3個引數的話,首先它是一個型別(如struct),其次要實現這個型別的 operator() 方法。

那麼,當雜湊表發生衝突(即傳入2個不同的key,但 std::hash<key> 返回的是相同的數字)的時候怎麼辦呢?unordered_map作為STL中雜湊表的一種實現,它當然有辦法來解決衝突(如開式定址法或拉鍊法,這個要看STL中的具體實現);而我們得讓 unordered_map 能夠知道為什麼這2個key不一樣。這就是第4個引數的作用了。

第4個引數預設傳入的是 std::equal_to<key> 函式物件,它帶2個引數,也就是2個key例項,然後返回一個bool變數表示這2個key是否相等。使用者可以自己特化這個 equal_to 函式物件,但因為預設的 std::equal_to<key> 會去呼叫 key 型別的 operator == 函式,所以使用者其實只要實現 key 型別的 operator == 函式就可以了。

基於以上對於 unordered_map 的模板引數的分析,想把 struct或class 作為 unordered_map 的 key,就需要做以下2件事情。

1. 建立特化的 std::hash<Key> 函式,以便於根據Key型別的例項來獲得在雜湊表中的位置。(作為第4個引數)

2. 定義 operator == 以便於在 Hash 衝突時比較真正的key值是否相等。(operator == 被第3個引數預設的equal_to使用)

程式碼例項有2個。第1個請見上篇部落格中的程式碼。第2個程式碼例項如下:

#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
 
template<typename T1, typename T2>
struct Node
{
    T1 x;
    T2 y;
 
    Node(T1 a, T2 b) : x(a), y(b) {}
 
    bool operator==(const Node& p) const {
        return x == p.x && y == p.y;
    }
};

// Way-1: specialized hash function for unordered_map keys
struct hash_fn
{
    template <class T1, class T2>
    std::size_t operator() (const Node<T1, T2> & node) const {
        std::size_t h1 = std::hash<T1>()(node.x);
        std::size_t h2 = std::hash<T2>()(node.y);
        return h1 ^ h2;
    }
};

// Way-2: 特化 std::hash<Node>
namespace std
{
    template<typename T1, typename T2>
    struct hash<Node<T1, T2>>
    {
        size_t operator() (const Node<T1, T2> & node) const noexcept
        {
            std::size_t h1 = std::hash<T1>()(node.x);
            std::size_t h2 = std::hash<T2>()(node.y);
            return h1 ^ h2;
        }
    };
}
 
int main()
{
    // Way-1 
    std::unordered_map< Node<std::string,std::string>, int, hash_fn  > u_map1 =
    {
        {{"C", "C99"}, 1999},
        {{"C", "C11"}, 2011},
        {{"C++", "C++14"}, 2014},
        {{"C++", "C++17"}, 2017},
        {{"Java", "Java SE 8"}, 2014},
        {{"Java", "Java SE 9"}, 2017}
    };
 
    cout << "Way-1: \n";
    for (const auto &entry: u_map1)
    {
        std::cout << "{" << entry.first.x << "," << entry.first.y << "}: "
                  << entry.second << '\n';
    }
    
    // Way-2 
    std::unordered_map< Node<std::string,std::string>, int > u_map2 =
    {
        {{"C", "C99"}, 1999},
        {{"C", "C11"}, 2011},
        {{"C++", "C++14"}, 2014},
        {{"C++", "C++17"}, 2017},
        {{"Java", "Java SE 8"}, 2014},
        {{"Java", "Java SE 9"}, 2017}
    };
    
    cout << "Way-2: \n";
    for (const auto &entry: u_map2)
    {
        std::cout << "{" << entry.first.x << "," << entry.first.y << "}: "
                  << entry.second << '\n';
    }
 
    return 0;
}

(完)
 

相關文章