目录
二. 模拟实现unorderedset/unorderedmap
引言
在C++标准模板库(STL)中,unordered_map和unordered_set是高效的无序关联容器,底层基于哈希表实现,提供平均O(1)时间复杂度的查找、插入和删除操作。这些容器在实际应用中广泛用于缓存、数据去重和快速查询场景。理解其内部机制有助于更好地优化代码和处理边缘情况。
一. 源码及框架分析
SGI-STL30版本源代码中没有unordered_map和unordered_set,SGI-STL30版本是C++11之前的STL 版本,这两个容器是C++11之后才更新的。但是SGI-STL30实现了哈希表,只容器的名字是hash_map和hash_set,他是作为非标准的容器出现的,非标准是指非C++标准规定必须实现的,源代码在 hash_map/hash_set/stl_hash_map/stl_hash_set/stl_hashtable.h中 hash_map和hash_set的实现结构框架核心部分截取出来如下:
// stl_hash_set
template <class Value, class HashFcn = hash<Value>,
class EqualKey = equal_to<Value>,
class Alloc = alloc>
class hash_set
{
private:
typedef hashtable<Value, Value, HashFcn, identity<Value>,
EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;
hasher hash_funct() const { return rep.hash_funct(); }
key_equal key_eq() const { return rep.key_eq(); }
};
// stl_hash_map
template <class Key, class T, class HashFcn = hash<Key>,
class EqualKey = equal_to<Key>,
class Alloc = alloc>
class hash_map
{
private:
typedef hashtable<pair<const Key, T>, Key, HashFcn,
select1st<pair<const Key, T> >, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef T data_type;
typedef T mapped_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::iterator iterator;
typedef typename ht::const_iterator const_iterator;
};
// stl_hashtable.h
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey,
class Alloc>
class hashtable {
public:
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*,Alloc> buckets;
size_type num_elements;
public:
typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey,
Alloc> iterator;
pair<iterator, bool> insert_unique(const value_type& obj);
const_iterator find(const key_type& key) const;
};
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};
通过源码可以看到,结构上hash_map和hash_set跟map和set的完全类似,复用同一个hashtable实现key和key/value结构,hash_set传给hash_table的是两个 key,hash_map传给hash_table的是pair<const key, value>
二. 模拟实现unorderedset/unorderedmap
2.1 实现出复用的哈希表

2.1.1 实现复用的哈希表的整体框架
和set/map类似,我们将哈希表中存储的节点类型定义为模板类型,set传递的模板参数是K,map传递的模板参数是pair<K,V>,这样,同一个哈希表就能实现复用
template<class T>
struct HashNode
{
T _data;
HashNode<T>* next;
HashNode(const T& data)
:_data(data)
,next(nullptr)
{}
};
2.1.2 实现KeyOfT仿函数
用上述方法修改之后,又会有一个新的问题,unordered_set中存储的是key,unordered_map中存储的是pair<K,V>,
为了让 unordered_map 和 unordered_set 复用代码,底层的 HashTable 必须足够通用。最大的难点在于:
-
Set 存储的是
Key。 -
Map 存储的是
pair<const Key, Value>。 -
哈希表在计算哈希值、比较相等时,都需要获取
Key。对于 Set,数据本身就是 Key;对于 Map,需要从 Pair 中提取 Key。
为了解决上述差异,我们在 HashTable 的模板参数中引入了一个 KeyOfT 仿函数策略
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
// ...
// K: 键的类型
// T: 存储的数据类型(Set是K,Map是pair<const K, V>)
// KeyOfT: 从T中提取K的仿函数
};
这样,上层容器只需传入不同的仿函数:
-
unordered_set: 传入SetKeyOfT,直接返回数据本身 -
unordered_map: 传入MapKeyOfT,返回pair<K,V>.first
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
// 内部类(把key取出来)
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
}
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
// 内部类(把key取出来)
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
}
2.1.3 迭代器的实现
1)底层迭代器的实现
哈希表的迭代器是单向迭代器(Forward Iterator)。它的难点在于如何在“断开”的桶之间进行遍历(operator++)。迭代器不仅需要指向当前的节点 _node,还需要持有哈希表对象 _pht 的指针。这是因为当一个桶遍历完后,需要通过哈希表对象找到下一个非空桶 。
iterator实现思路分析:
- iterator实现的大框架跟list的iterator思路是一致的,用一个类型封装结点的指针,再通过重载运算符实现,迭代器像指针一样访问的行为,要注意的是哈希表的迭代器是单向迭代器。
- 这里的难点是operator++的实现。iterator中有一个指向结点的指针,如果当前桶下面还有结点, 则结点的指针指向下一个结点即可。如果当前桶走完了,则需要想办法计算找到下一个桶。这里的 难点是反而是结构设计的问题,参考上面的源码,我们可以看到iterator中除了有结点的指针,还 有哈希表对象的指针,这样当前桶走完了,要计算下一个桶就相对容易多了,用key值计算出当前桶位置,依次往后找下一个不为空的桶即可。
- begin()返回第一个桶中第一个节点指针构造的迭代器,这里end()返回迭代器可以用空表示。
- unordered_set的iterator不支持修改,我们把unordered_set的第二个模板参数改成const K即可, HashTable<K, const K, SetKeyOfT, Hash> _ht;
- unordered_map的iterator不支持修改key但是可以修改value,我们把unordered_map的第二个模板参数pair的第一个参数改成const K即可, HashTable<K, pair<K,V>, MapKeyOfT, Hash> _ht;
operator++的逻辑:
-
如果当前节点的
_next不为空,直接指向下一个节点。 -
如果当前桶已经走完(
_next为空),则需要计算当前 Key 的哈希值,找到当前桶的索引。 -
从当前索引的下一个位置开始,线性探测
_tables,直到找到第一个非空桶,或者走完所有桶

Self operator++()
{
// 当前桶没走完
if (_node->next)
{
_node = _node->next;
}
// 当前桶没走完,找到下一个桶的第一个节点(哈希表的指针就是在这里用的)
else
{
KeyOfT kot;
Hash hs;
// 先算出当前在那个桶
size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();
// 要从当前桶的下一个桶开始找
++hashi;
while (hashi < _pht->_tables.size())
{
// 找到下一个不为空的桶
if (_pht->_tables[hashi])
{
_node = _pht->_tables[hashi];
break;
}
else
{
++hashi;
}
}
// 最后一个桶走完了,要++到end()迭代器位置
if (hashi == _pht->_tables.size())
{
// end()中的_node是空
_node = nullptr;
}
}
return *this;
}
这里只实现了比较复杂的operator++(),完整代码详见:底层迭代器的实现
2)哈希表中封装迭代器
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
// 友元声明:类模板当友元,要加上模板参数
// 让迭代器成为哈希表的友元,因为在迭代器中会访问哈希表的私有成员变量_tables;另一种方法是写GetTable函数
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HtIterator;
typedef HashNode<T> Node;
public:
typedef HtIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
typedef HtIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;
Iterator Begin()
{
// 找到第一个不为空的桶
for (size_t i = 0; i < _tables.size(); i++)
{
// 如果没有数据就直接return End(),不用再遍历了
if (_n == 0)
{
return End();
}
if (_tables[i])
{
// 用节点的指针和哈希表的指针构造;this就是哈希表的指针
return Iterator(_tables[i], this);
}
}
// 没有找到
return End();
}
Iterator End()
{
return Iterator(nullptr, this);
}
ConstIterator Begin() const
{
// 找到第一个不为空的桶
for (size_t i = 0; i < _tables.size(); i++)
{
// 如果没有数据就直接return End(),不用再遍历了
if (_n == 0)
{
return End();
}
if (_tables[i])
{
// 用节点的指针和哈希表的指针构造;this就是哈希表的指针
return ConstIterator(_tables[i], this); // 这里的this类型是const HT*,所以HtIterator的构造函数中,哈希表的指针要const
}
}
// 没有找到
return End();
}
ConstIterator End() const
{
return ConstIterator(nullptr, this); // 这里的this类型是const HT*,所以HtIterator的构造函数中,哈希表的指针要const
}
}
2.2 封装哈希表实现unorderedset
unordered_set 的核心是将 Key 既作为 Key 也作为 Value。需要注意的是,Set 中的元素是不可修改的,因此我们传递给 HashTable 的模板参数是 const K 。
2.3 封装哈希表实现unorderedmap
unordered_map 存储的是键值对。
- 数据类型:
pair<const K, V>(Key不可改,Value可改)。 -
Key提取:使用
MapKeyOfT返回kv.first。
2.3.1 实现operator[ ]
unordered_map 的 [] 运算符非常强大:如果 Key 存在则返回 Value 的引用,不存在则插入默认值并返回引用。这依赖于底层的 Insert 函数返回类型 pair<iterator, bool>
V& operator[](const K& key) {
// 尝试插入 {key, 默认值}
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
// 无论插入成功还是失败,ret.first 都指向该 Key 所在的节点
return ret.first->second;
}
2.3.2 完整代码实现
#include"HashTable.h"
namespace My
{
// 这里的仿函数要放在上层,否则传仿函数传不过去
// 其实还应该加一个和key比较相等的仿函数,这里没加
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
// 内部类(把key取出来)
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
pair<iterator, bool> insert(const pair<K,V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert({ key,V() });
return ret.first->second;
}
iterator find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
}
结语
如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!
1047

被折叠的 条评论
为什么被折叠?



