簡體   English   中英

C++ unordered_map 使用自定義 class 類型作為鍵

[英]C++ unordered_map using a custom class type as the key

我正在嘗試使用自定義 class 作為unordered_map的鍵,如下所示:

#include <iostream>
#include <algorithm>
#include <unordered_map>

using namespace std;

class node;
class Solution;

class Node {
public:
    int a;
    int b; 
    int c;
    Node(){}
    Node(vector<int> v) {
        sort(v.begin(), v.end());
        a = v[0];       
        b = v[1];       
        c = v[2];       
    }

    bool operator==(Node i) {
        if ( i.a==this->a && i.b==this->b &&i.c==this->c ) {
            return true;
        } else {
            return false;
        }
    }
};

int main() {
    unordered_map<Node, int> m;    

    vector<int> v;
    v.push_back(3);
    v.push_back(8);
    v.push_back(9);
    Node n(v);

    m[n] = 0;

    return 0;
}

但是,g++ 給我以下錯誤:

In file included from /usr/include/c++/4.6/string:50:0,
                 from /usr/include/c++/4.6/bits/locale_classes.h:42,
                 from /usr/include/c++/4.6/bits/ios_base.h:43,
                 from /usr/include/c++/4.6/ios:43,
                 from /usr/include/c++/4.6/ostream:40,
                 from /usr/include/c++/4.6/iostream:40,
                 from 3sum.cpp:4:
/usr/include/c++/4.6/bits/stl_function.h: In member function ‘bool std::equal_to<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = Node]’:
/usr/include/c++/4.6/bits/hashtable_policy.h:768:48:   instantiated from ‘bool std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, std::__detail::_Default_ranged_hash, false>::_M_compare(const _Key&, std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, std::__detail::_Default_ranged_hash, false>::_Hash_code_type, std::__detail::_Hash_node<_Value, false>*) const [with _Key = Node, _Value = std::pair<const Node, int>, _ExtractKey = std::_Select1st<std::pair<const Node, int> >, _Equal = std::equal_to<Node>, _H1 = std::hash<Node>, _H2 = std::__detail::_Mod_range_hashing, std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, std::__detail::_Default_ranged_hash, false>::_Hash_code_type = long unsigned int]’
/usr/include/c++/4.6/bits/hashtable.h:897:2:   instantiated from ‘std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Node* std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_M_find_node(std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Node*, const key_type&, typename std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Hash_code_type) const [with _Key = Node, _Value = std::pair<const Node, int>, _Allocator = std::allocator<std::pair<const Node, int> >, _ExtractKey = std::_Select1st<std::pair<const Node, int> >, _Equal = std::equal_to<Node>, _H1 = std::hash<Node>, _H2 = std::__detail::_Mod_range_hashing, _Hash = std::__detail::_Default_ranged_hash, _RehashPolicy = std::__detail::_Prime_rehash_policy, bool __cache_hash_code = false, bool __constant_iterators = false, bool __unique_keys = true, std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Node = std::__detail::_Hash_node<std::pair<const Node, int>, false>, std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::key_type = Node, typename std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Hash_code_type = long unsigned int]’
/usr/include/c++/4.6/bits/hashtable_policy.h:546:53:   instantiated from ‘std::__detail::_Map_base<_Key, _Pair, std::_Select1st<_Pair>, true, _Hashtable>::mapped_type& std::__detail::_Map_base<_Key, _Pair, std::_Select1st<_Pair>, true, _Hashtable>::operator[](const _Key&) [with _Key = Node, _Pair = std::pair<const Node, int>, _Hashtable = std::_Hashtable<Node, std::pair<const Node, int>, std::allocator<std::pair<const Node, int> >, std::_Select1st<std::pair<const Node, int> >, std::equal_to<Node>, std::hash<Node>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, false, false, true>, std::__detail::_Map_base<_Key, _Pair, std::_Select1st<_Pair>, true, _Hashtable>::mapped_type = int]’
3sum.cpp:149:5:   instantiated from here
/usr/include/c++/4.6/bits/stl_function.h:209:23: error: passing ‘const Node’ as ‘this’ argument of ‘bool Node::operator==(Node)’ discards qualifiers [-fpermissive]
make: *** [threeSum] Error 1

我想,我需要告訴 C++ 如何 hash class Node ,但是,我不太確定該怎么做。 我怎樣才能完成這個任務?

為了能夠將std::unordered_map (或其他無序關聯容器之一)與用戶定義的鍵類型一起使用,您需要定義兩件事:

  1. 一個哈希函數 這必須是一個覆蓋operator()並計算給定鍵類型對象的哈希值的類。 一種特別直接的方法是為您的鍵類型專門化std::hash模板。

  2. 相等比較函數 這是必需的,因為散列不能依賴這樣一個事實,即散列函數將始終為每個不同的鍵提供唯一的散列值(即,它需要能夠處理沖突),因此它需要一種方法來比較兩個給定的鍵精確匹配。 您可以將其實現為覆蓋operator()的類,或者作為std::equal ,或者 - 最簡單的 - 通過為您的鍵類型重載operator==() (就像您已經做過的那樣)。

散列函數的困難在於,如果您的鍵類型由多個成員組成,您通常會讓散列函數計算各個成員的散列值,然后以某種方式將它們組合成整個對象的一個​​散列值。 為了獲得良好的性能(即,很少發生沖突),您應該仔細考慮如何組合各個散列值,以確保避免頻繁為不同的對象獲得相同的輸出。

散列函數的一個相當好的起點是使用位移位和按位異或來組合各個散列值。 例如,假設這樣的鍵類型:

struct Key
{
  std::string first;
  std::string second;
  int         third;

  bool operator==(const Key &other) const
  { return (first == other.first
            && second == other.second
            && third == other.third);
  }
};

這是一個簡單的哈希函數(改編自cppreference 示例中用於用戶定義的哈希函數的函數):

namespace std {

  template <>
  struct hash<Key>
  {
    std::size_t operator()(const Key& k) const
    {
      using std::size_t;
      using std::hash;
      using std::string;

      // Compute individual hash values for first,
      // second and third and combine them using XOR
      // and bit shifting:

      return ((hash<string>()(k.first)
               ^ (hash<string>()(k.second) << 1)) >> 1)
               ^ (hash<int>()(k.third) << 1);
    }
  };

}

有了這個,您可以為鍵類型實例化一個std::unordered_map

int main()
{
  std::unordered_map<Key,std::string> m6 = {
    { {"John", "Doe", 12}, "example"},
    { {"Mary", "Sue", 21}, "another"}
  };
}

它將自動使用上面定義的std::hash<Key>進行哈希值計算,並將operator==定義為Key成員函數進行相等性檢查。

如果您不想在std命名空間內專門化模板(盡管在這種情況下它是完全合法的),您可以將哈希函數定義為一個單獨的類並將其添加到映射的模板參數列表中:

struct KeyHasher
{
  std::size_t operator()(const Key& k) const
  {
    using std::size_t;
    using std::hash;
    using std::string;

    return ((hash<string>()(k.first)
             ^ (hash<string>()(k.second) << 1)) >> 1)
             ^ (hash<int>()(k.third) << 1);
  }
};

int main()
{
  std::unordered_map<Key,std::string,KeyHasher> m6 = {
    { {"John", "Doe", 12}, "example"},
    { {"Mary", "Sue", 21}, "another"}
  };
}

如何定義更好的哈希函數? 如上所述,定義一個好的散列函數對於避免沖突和獲得良好的性能很重要。 對於真正好的一個,您需要考慮所有字段的可能值的分布,並定義一個散列函數,將該分布投影到盡可能寬且分布均勻的可能結果空間。

這可能很困難; 上面的異或/位移方法可能不是一個糟糕的開始。 對於一個稍微好一點的開始,您可以使用hash_valuehash_combine函數模板從Boost庫。 前者的作用與標准類型的std::hash類似(最近還包括元組和其他有用的標准類型); 后者可幫助您將單個哈希值合並為一個。 這是使用 Boost 輔助函數的哈希函數的重寫:

#include <boost/functional/hash.hpp>

struct KeyHasher
{
  std::size_t operator()(const Key& k) const
  {
      using boost::hash_value;
      using boost::hash_combine;

      // Start with a hash value of 0    .
      std::size_t seed = 0;

      // Modify 'seed' by XORing and bit-shifting in
      // one member of 'Key' after the other:
      hash_combine(seed,hash_value(k.first));
      hash_combine(seed,hash_value(k.second));
      hash_combine(seed,hash_value(k.third));

      // Return the result.
      return seed;
  }
};

這是一個不使用 boost 的重寫,但使用了組合散列的好方法:

namespace std
{
    template <>
    struct hash<Key>
    {
        size_t operator()( const Key& k ) const
        {
            // Compute individual hash values for first, second and third
            // http://stackoverflow.com/a/1646913/126995
            size_t res = 17;
            res = res * 31 + hash<string>()( k.first );
            res = res * 31 + hash<string>()( k.second );
            res = res * 31 + hash<int>()( k.third );
            return res;
        }
    };
}

我認為, jogojapan給出了一個非常好的和詳盡的答案 在閱讀我的帖子之前,您絕對應該先看看它。 但是,我想添加以下內容:

  1. 您可以為unordered_map單獨定義一個比較函數,而不是使用相等比較運算符 ( operator== )。 這可能會有所幫助,例如,如果您想使用后者來比較兩個Node對象的所有成員,但只有一些特定成員作為unordered_map鍵。
  2. 您還可以使用lambda 表達式而不是定義散列和比較函數。

總而言之,對於你的Node類,代碼可以寫成如下:

using h = std::hash<int>;
auto hash = [](const Node& n){return ((17 * 31 + h()(n.a)) * 31 + h()(n.b)) * 31 + h()(n.c);};
auto equal = [](const Node& l, const Node& r){return l.a == r.a && l.b == r.b && l.c == r.c;};
std::unordered_map<Node, int, decltype(hash), decltype(equal)> m(8, hash, equal);

筆記:

  • 我只是在 jogojapan 的答案末尾重用了哈希方法,但是您可以在此處找到更通用的解決方案的想法(如果您不想使用 Boost)。
  • 我的代碼可能有點過於縮小了。 有關可讀性稍強的版本,請參閱Ideone 上的此代碼

使用自定義類作為unordered_map鍵(稀疏矩陣的基本實現)的最基本的可能復制/粘貼完整的可運行示例:

// UnorderedMapObjectAsKey.cpp

#include <iostream>
#include <vector>
#include <unordered_map>

struct Pos
{
  int row;
  int col;

  Pos() { }
  Pos(int row, int col)
  {
    this->row = row;
    this->col = col;
  }

  bool operator==(const Pos& otherPos) const
  {
    if (this->row == otherPos.row && this->col == otherPos.col) return true;
    else return false;
  }

  struct HashFunction
  {
    size_t operator()(const Pos& pos) const
    {
      size_t rowHash = std::hash<int>()(pos.row);
      size_t colHash = std::hash<int>()(pos.col) << 1;
      return rowHash ^ colHash;
    }
  };
};

int main(void)
{
  std::unordered_map<Pos, int, Pos::HashFunction> umap;

  // at row 1, col 2, set value to 5
  umap[Pos(1, 2)] = 5;

  // at row 3, col 4, set value to 10
  umap[Pos(3, 4)] = 10;

  // print the umap
  std::cout << "\n";
  for (auto& element : umap)
  {
    std::cout << "( " << element.first.row << ", " << element.first.col << " ) = " << element.second << "\n";
  }
  std::cout << "\n";

  return 0;
}

對於枚舉類型,我認為這是一種合適的方式,而類之間的區別在於如何計算哈希值。

template <typename T>
struct EnumTypeHash {
  std::size_t operator()(const T& type) const {
    return static_cast<std::size_t>(type);
  }
};

enum MyEnum {};
class MyValue {};

std::unordered_map<MyEnum, MyValue, EnumTypeHash<MyEnum>> map_;

STL 不提供對的散列函數。 您需要自己實現它並指定為模板參數或放入命名空間 std,從那里它會被自動拾取。 遵循https://github.com/HowardHinnant/hash_append/blob/master/n3876.h對於為結構實現自定義哈希函數非常有用。 更多細節在這個問題的其他答案中有很好的解釋,所以我不會重復。 Boost 中也有類似的東西( hash_combine )。

查看以下鏈接https://www.geeksforgeeks.org/how-to-create-an-unordered_map-of-user-defined-class-in-cpp/了解更多詳情。

  • 自定義類必須實現 == 運算符
  • 必須為類創建一個散列函數(對於像 int 這樣的原始類型和像 string 這樣的類型,散列函數是預定義的)

這里的答案非常有幫助,但我仍在努力嘗試解決這個問題,所以也許我的經驗教訓會有所幫助。 與 OP 相比,我的情況有點獨特; 我的key我不擁有的自定義 UUID class。 在我認為的錯誤/疏忽中,這個 class沒有定義 hash function 或operator()的重載(它確實定義了operator== ,所以我被設置在那里)。 是的,我有源代碼,但它被廣泛分發和控制,所以修改它是行不通的。 我想將此 UUID 用作std::unordered_map成員中的鍵,例如

std::unordered_map<UUID, MyObject> mapOfObjs_;

在 Visual Studio 中,我最終選擇了這個解決方案:

// file MyClass.h

namespace myNamespace
{
   static auto staticUuidHashFunc = [](const UUID& n)
   {
      // XORed the most and least significant bits, not important
   }
   ... 
   class MyClass
   {
      ...
   private:
      std::unordered_map<UUID, std::unique_ptr<MyObject>, decltype(staticUuidHashFunc)> mapOfObjs_;
   };
}

這在 Windows 中非常有效。但是,當我最終在 linux 中將我的代碼帶到 gcc 時,我收到了警告(釋義)

'MyClass'有一個字段'mapOfObjs_' ,其類型使用匿名命名空間

我什至在禁用所有警告的情況下收到此警告,因此 gcc 必須認為它非常嚴重。 我四處搜索並找到了這個答案,這表明我需要將 hash function 代碼移動到 .cpp 文件。

此時,我還嘗試從 UUID class 派生:

// file MyClass.h

namespace myNamespace
{
   struct myUuid : public UUID
   {
      // overload the operator()
   };
   ...
   // and change my map to use this type
   std::unordered_map<myUuid, std::unique_ptr<MyObject>> mapOfObjs_;
}

然而,這也帶來了一系列問題。 即,使用(現在的父級) UUID class 的所有代碼部分都與我的 map 不兼容,例如:

void MyClass::FindUuid(const UUID& id)
{
   // doesn't work, can't convert `id` to a `myUuid` type
   auto it = mapOfObjs_.find(id);
   ...
}

現在壞了。 我不想更改所有這些代碼,所以我放棄了它並回到了“將代碼放入 .cpp 文件”的解決方案。 然而,頑固地,我仍然嘗試了一些方法來將 hash function 保留在 .h 文件中。 我真正想要避免的是從 hash function 定義中刪除auto ,因為我不知道也不想弄清楚類型是什么。 所以我嘗試了:

class MyClass
{
   ...
private:
   static auto staticUuidHashFunc = [](const UUID& n)
   {
      // my hash function
   }
};

但是這個(或這個的變體)返回錯誤,比如“類中不能有 static 初始值設定項”,“不能在這里使用auto ”等(我有一個硬性的 C++11 要求)。 所以我終於接受我需要像對待static變量一樣對待它,在 header 中聲明它,並在 .cpp 文件中初始化它。 一旦我弄清楚了它的類型,它就很簡單了:

// MyClass.h
namespace myNamespace
{
   class MyClass
   {
      ...
   private:
      static std::function<unsigned long long(const UUID&)> staticUuidHashFunc;

      std::unordered_map<UUID, std::unique_ptr<MyObject>, decltype(staticUuidHashFunc)> mapOfObjs_;
   };
}

最后在 .cpp 文件中:

// MyClass.cpp

namespace myNamespace
{
   std::function<unsigned long long(const UUID&)> MyClass::staticUuidHashFunc = [](const UUID& n)
    {
        // the hash function
    };

    MyClass::MyClass()
       : mapOfObjs_{ std::unordered_map<UUID, std::unique_ptr<MyObject>, decltype(staticUuidHashFunc)> (MyClass::NUMBER_OF_MAP_BUCKETS, staticUuidHashFunc)}
    {  }

   ...
}

在.cpp文件中定義static hash function是關鍵。 之后,Visual Studio 和 gcc 都很開心。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM