一、自定義鍵值的方法和源碼
使用自定義類型(非基本類型)作為 unordered_map 的鍵值時,則必須為自定義類型定義Hash 函數與相等的判斷條件。在網上找了說明,自己在VS2013上運行無誤,一下博文來自轉載。
#pragma once #include<unordered_map> using namespace std; //自定義鍵值類型 struct KEY { int first; int second; int third; KEY(int f, int s, int t) : first(f), second(s), third(t){} }; /*一、自定義Hash函數: 必須為 override 了 operator() 的一個類,一般自定義類型可能包含幾種內置類型, *我們可以分別計算出內置類型的 Hash Value 然后對它們進行 Combine 得到一個哈希值, *一般直接采用移位加異或(XOR)便可得到還不錯的哈希值(碰撞不會太頻繁); */ struct HashFunc { std::size_t operator()(const KEY &key) const { using std::size_t; using std::hash; return ((hash<int>()(key.first) ^ (hash<int>()(key.second) << 1)) >> 1) ^ (hash<int>()(key.third) << 1); } }; /*---------------------------------------------------------------------------------------- 另外一種方法是直接實例化模板,這樣的話使用 unordered_map 時便不用再指定 Hash 函數, 但要求必須為 KEY 重載 operator ==,實例化模板如下: -----------------------------------------------------------------------------------------*/ namespace std { template <> struct hash<KEY> { std::size_t operator()(const KEY &key) const { using std::size_t; using std::hash; // Compute individual hash values for first, // second and third and combine them using XOR // and bit shifting: return ((hash<int>()(key.first) ^ (hash<int>()(key.second) << 1)) >> 1) ^ (hash<int>()(key.third) << 1); } }; } /* 二、相等函數:哈希需要處理碰撞,意味着必須得知道兩個自定義類型對象是否相等,所以必須得提供比較相等的方法,
可以 overload operator ==,可以用 std::equal,也可以實現一個 override operator () 的類,
這里我們采用后者(這也意味着 Hash 函數不能使用實例化模板的方法,只能定義一個重載了 operator() 的類)
*/
struct EqualKey { bool operator () (const KEY &lhs, const KEY &rhs) const { return lhs.first == rhs.first && lhs.second == rhs.second && lhs.third == rhs.third; } };
// ServerStudy.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" #include <iostream> #include <stdlib.h> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { unordered_map<KEY, string, HashFunc, EqualKey> hashmap = { { { 01, 02, 03 }, "one" }, { { 11, 12, 13 }, "two" }, { { 21, 22, 23 }, "three" }, }; KEY key(11, 12, 13); auto it = hashmap.find(key); if (it != hashmap.end()) { cout << it->second.c_str() << endl; } system("pause"); return 0; }
二、關於Lambda實現Hash函數的說明
C++ STL中的unordered_map底層是通過Hash實現的,當使用pair作為鍵值(Key)時,需要手動傳入Hash實例類型,轉載自其它。
1. 函數對象的實現方式
參考網上的解決方案,通過一個函數對象向unordered_map傳遞Hash實例類型。具體實現如下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <unordered_map>
using namespace std; struct hashfunc { template<typename T, typename U> size_t operator()(const pair<T, U> &i) const { return hash<T>()(x.first) ^ hash<U>()(x.second); } }; int main() { unordered_map<pair<int, int>, int, hashfunc> func_map; func_map[make_pair(1, 2)]++; return 0; } |
在這中解決方案中,我們創建了一個函數對象類hashfunc
,並將它作為unordered_map
的Hash實例類型傳入,成功構造以pair為Key的無序容器。
2. Lambda如何實現呢?
Lambda是C++的一種新特性,既然可以通過函數對象實現上面的功能。自然,我們會聯想用lambda表達式去實現上面的功能。最初的實現想法:
1
2
3
4
5
6
|
int main() { auto hashlambda = [](const pair<int, int>& i) -> size_t{return i.first ^ i.second;}; unordered_map<pair<int, int>, int, decltype(hashlambda)> lam_map; lam_map[make_pair(1, 2)]++; return 0; } |
進行編譯出錯,提示“lambda默認構造函數是刪除的(deleted)”。為什么會這樣,同樣是可調用對象,為何lambda實現時無法通過編譯呢?
自己一翻折騰並大牛的熱心幫助下終於有所明白,簡單說來,unordered_map
繼承自_Hash類型,_Hash用到一個_Uhash_compare類來封裝傳入的hash函數,如果unordered_map
構造函數沒有顯示的傳入hash函數實例引用,則unordered_map
默認構造函數使用第三個模板參數指定的Hash類型的默認構造函數,進行hash函數實例的默認構造。在第一種情況中,編譯為函數類型合成默認構造函數也就是hash_fun(),所以我們在定義unordered_map
時即使不傳入函數對象實例,也能通過默認構造函數生成。但是,對於lambda對象來說,雖然編譯時會為每個lambda表達式產生一個匿名類,但是這個匿名類時不含有默認構造函數(=deleted)。因此,如果實例化unordered_map
時,不傳入lambda對象實例引用,默認構造函數不能為我們合成一個默認的hash函數實例。所以,編譯時產生了上面的錯誤。明白了這些,自然知道如何去修改了。
1 2 3 4 5 6 |
int main() { auto hashlambda = [](const pair<int, int>& i) -> size_t{return i.first ^ i.second;}; unordered_map<pair<int, int>, int, decltype(hashlambda)> lam_map(10, hashlambda); lam_map[make_pair(1, 2)]++; return 0; } |
我們在創建unordered_map
對象時,手動指定了兩個參數;第一參數是“桶”的數量,第二個就是hash實例引用了。在這里需要留意的是,lambda雖然與函數類型功能相似,但在構造函數、賦值運算符、默認析構函數的限制是不同的。此外,還應該留意每個lambda表達都是一種類型,就是值引用、參數類型,返回值都一樣,但是它們的類型是不同的。
3. Functional可以通過編譯,但存在運行時錯誤
針對上面lambda功能實現時存在的編譯錯誤,有一種方法也可以避免編譯出錯。用function對象來保存lambda表達式:
1 2 3 4 5 6 |
int main() { function<size_t (const pair<int, int>&)> hashfuna = [](const pair<int, int>& i) -> size_t{return i.first ^ i.second;}; unordered_map<pair<int, int>, int, decltype(hashfuna)> lam_map; lam_map[make_pair(1, 2)]++; return 0; } |
可以發現,此時編譯一切正常,但執行時報錯。這是為什么呢?其實結合上面的解釋,結論也很顯然了。編譯出錯是因為我們指定的模版Hash類型,無法默認構造實例;但是用function保存lambda表達式后,這個function對象對映的類型是function<size_t (const pair<int, int>&)>
,它是有默認構造函數的,故而unordered_map
可以默認實例化成功,編譯正確。但是,function<size_t (const pair<int, int>&)>
的默認構造函數只會構造一個空的function,所以我們還是要如對待lambda對象那樣,手動傳入function對象引用(hashfuna)。
1 |
unordered_map<pair<int, int>, int, decltype(hashfuna)> lam_map(10, hashfuna); |
你可能會奇怪為何function<size_t (const pair<int, int>&)>
只構造了空的function對象呢,其實這也很顯然,functional只是為了通用的存儲“可調用對象”,所以它只能默認構造為空function。
1 2 3 4 5 |
function<size_t (const pair<int, int>&)> a = [](const pair<int, int>& i) -> size_t{return i.first ^ i.second;}; function<size_t (const pair<int, int>&)> b = [](const pair<int, int>& i) -> size_t{return 5;}; function<size_t (const pair<int, int>&)> c = 合法的函數指針; |
幾種可調用對象的不同之處希望對大家有所幫助,Lambda類型的解釋也希望對大家有所啟發!