写在前面
(本专栏仅是个人笔记本,有胡言乱语和错漏部分)
本文以图文+代码的形式记录了_Hashtable的结构,如何编排每一个bucket的链表,如何将每个bucket的链表串在一起形成一个长链表,如何利用迭代器遍历_Hashtable。
此外,还解释了insert操作的流程,以及为什么cache hash值可以提高_Hashtable的效率。
_Hashtable的数据成员
312 private:313 __bucket_type* _M_buckets = &_M_single_bucket;314 size_type _M_bucket_count = 1;315 __node_base _M_before_begin;316 size_type _M_element_count = 0;317 _RehashPolicy _M_rehash_policy;
_Bucket[] _M_buckets
数组,数组元素类型为Node*_Hash_node_base _M_before_begin
容器的哨兵节点size_type _M_bucket_count
bucket的数量,若采用Prime_rehash,则这个数一直是个素数size_type _M_element_count
容器中元素的数量
Node
268 template<typename _Value, bool _Cache_hash_code>269 struct _Hash_node;
有如下的数据成员:
_Hash_node_base* _M_nxt;
__gnu_cxx::__aligned_buffer<_Value> _M_storage;
为什么不直接用_Value v呢?- (可选,通过特例化添加)
std::size_t _M_hash_code;
当cache时,存在这个数据成员。
迭代器
对于(multi)set,返回的迭代器只能是只读的:
341 typedef std::forward_iterator_tag iterator_category;342 343 using pointer = typename std::conditional<__constant_iterators,344 const _Value*, _Value*>::type; //根据模板参数__constant_iterators决定是否const345 346 using reference = typename std::conditional<__constant_iterators,347 const _Value&, _Value&>::type; //根据模板参数__constant_iterators决定是否const
其他部分,跟forward_list的迭代器大同小异。
_ExtractKey
STL预设了两种从_Value中提取key的方法:
88 struct _Identity //for set89 {90 template<typename _Tp>91 _Tp&&92 operator()(_Tp&& __x) const93 { return std::forward<_Tp>(__x); }94 };95 96 struct _Select1st // for map97 {98 template<typename _Tp>99 auto100 operator()(_Tp&& __x) const101 -> decltype(std::get<0>(std::forward<_Tp>(__x)))102 { return std::get<0>(std::forward<_Tp>(__x)); }103 };104
_Hashtable的结构
_Hashtable的可以看作一个vector ,每一个bucket存储的是一个Node* 类型的指针,并具有如下特点:
- 当该bucket为空时,该bucket指针为null
- 每个哈希容器有一个哨兵节点(_M_before_begin),作为使用迭代器遍历容器的起点。
- 每一个bucket链表的最后一个结点,同时又是某一个bucket的哨兵节点。
用图描绘一下就是:
insert
insert定义在_Insert_base中,4种哈希容器公用一个insert函数:
710 __ireturn_type //ireturn_type对每个哈希容器不同711 insert(const value_type& __v)712 {713 __hashtable& __h = _M_conjure_hashtable();714 __node_gen_type __node_gen(__h);715 return __h._M_insert(__v, __node_gen, __unique_keys()); // false_type or true_type716 }
然后根据 __unique_keys(),调用不同重载版本的_M_insert():
//for multiset/multimap687 template<typename _Arg, typename _NodeGenerator>688 iterator689 _M_insert(_Arg&& __arg, const _NodeGenerator& __node_gen,690 std::false_type __uk)691 {692 return _M_insert(cend(), std::forward<_Arg>(__arg), __node_gen,693 __uk);694 }//for map/set1683 // Insert v if no element with its key is already present.1684 template<typename _Key, typename _Value,1685 typename _Alloc, typename _ExtractKey, typename _Equal,1686 typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,1687 typename _Traits>1688 template<typename _Arg, typename _NodeGenerator>1689 auto1690 _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,1691 _H1, _H2, _Hash, _RehashPolicy, _Traits>::1692 _M_insert(_Arg&& __v, const _NodeGenerator& __node_gen, std::true_type)1693 -> pair<iterator, bool>1694 {1695 const key_type& __k = this->_M_extract()(__v);1696 __hash_code __code = this->_M_hash_code(__k);1697 size_type __bkt = _M_bucket_index(__k, __code);1698 1699 __node_type* __n = _M_find_node(__bkt, __k, __code);1700 if (__n)1701 return std::make_pair(iterator(__n), false);1702 1703 __n = __node_gen(std::forward<_Arg>(__v));1704 return std::make_pair(_M_insert_unique_node(__bkt, __code, __n), true);1705 }
这里我们暂时只关注unordered_map/set的插入。
在插入之前,首先在bucket中查找,先用 _M_find_node判断是否存在key相同的元素, _M_find_node又是基于_M_find_before_node的,其代码如下。
1413 // Find the node whose key compares equal to k in the bucket n.1414 // Return nullptr if no node is found.1415 template<typename _Key, typename _Value,1416 typename _Alloc, typename _ExtractKey, typename _Equal,1417 typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,1418 typename _Traits>1419 auto1420 _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,1421 _H1, _H2, _Hash, _RehashPolicy, _Traits>::1422 _M_find_before_node(size_type __n, const key_type& __k,1423 __hash_code __code) const1424 -> __node_base*1425 {1426 __node_base* __prev_p = _M_buckets[__n];1427 if (!__prev_p)1428 return nullptr;1429 1430 for (__node_type* __p = static_cast<__node_type*>(__prev_p->_M_nxt);;1431 __p = __p->_M_next())1432 {1433 if (this->_M_equals(__k, __code, __p))1434 return __prev_p;1435 //由于_Hashtable的结构特殊,每次都要判断是否已经遍历完了当前的bucket1436 if (!__p->_M_nxt || _M_bucket_index(__p->_M_next()) != __n)1437 break;1438 __prev_p = __p;1439 }1440 return nullptr;1441 }
在1436行会频繁调用_M_bucket_index(Node *)
这个函数,如果cache了hash值,_M_bucket_index仅需一次mod操作,否则就是extract_key, hash, mod
这三个操作都要进行。这就是cache hash值的意义。GCC注释中也是这么解释的:
Walking through a bucket's nodes requires a check on the hash code to see if each node is still in the bucket. Such a design assumes a quite efficient hash functor and is one of the reasons it is highly advisable to set __cache_hash_code to true.
如果不存在相同key的元素,_M_insert进而调用了_M_insert_unique_node执行插入:
_M_insert_unique_node(size_type __bkt, __hash_code __code, __node_type* __node)
函数功能:已经确定表中没有相同key的元素,向表中插入新的元素。
//无特例化1581 template<typename _Key, typename _Value,1582 typename _Alloc, typename _ExtractKey, typename _Equal,1583 typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,1584 typename _Traits>1585 auto1586 _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,1587 _H1, _H2, _Hash, _RehashPolicy, _Traits>::1588 _M_insert_unique_node(size_type __bkt, __hash_code __code,1589 __node_type* __node)1590 -> iterator1591 {//交由_RehashPolicy 判断 是否需要rehash,1592 const __rehash_state& __saved_state = _M_rehash_policy._M_state();1593 std::pair<bool, std::size_t> __do_rehash1594 = _M_rehash_policy._M_need_rehash(_M_bucket_count, _M_element_count, 1); //1 表示要插入一个元素1595 1596 __try1597 {1598 if (__do_rehash.first) //需要rehash1599 {1600 _M_rehash(__do_rehash.second, __saved_state);1601 __bkt = _M_bucket_index(this->_M_extract()(__node->_M_v()), __code);1602 }1603 1604 this->_M_store_code(__node, __code); //在node中保存哈希值1605 1606 // Always insert at the beginning of the bucket.1607 _M_insert_bucket_begin(__bkt, __node);1608 ++_M_element_count; //哈希表中的总元素数目1609 return iterator(__node);1610 }1611 __catch(...)1612 {1613 this->_M_deallocate_node(__node);1614 __throw_exception_again;1615 }1616 }
_M_insert_unique_node又调用了_M_insert_bucket_begin
。
_M_insert_bucket_begin(size_type __bkt, __node_type* __node)
函数作用:在bucket处的链表头部插入新节点,若这是该bucket的第一个节点,则会有额外的指针操作。 结合注释并参考上面的图,还是挺好理解的
1443 template<typename _Key, typename _Value,1444 typename _Alloc, typename _ExtractKey, typename _Equal,1445 typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,1446 typename _Traits>1447 void1448 _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,1449 _H1, _H2, _Hash, _RehashPolicy, _Traits>::1450 _M_insert_bucket_begin(size_type __bkt, __node_type* __node)1451 {1452 if (_M_buckets[__bkt])1453 {1454 // Bucket is not empty, we just need to insert the new node1455 // after the bucket before begin.1456 __node->_M_nxt = _M_buckets[__bkt]->_M_nxt;1457 _M_buckets[__bkt]->_M_nxt = __node;1458 }1459 else1460 {1461 // The bucket is empty, the new node is inserted at the1462 // beginning of the singly-linked list and the bucket will1463 // contain _M_before_begin pointer.1464 __node->_M_nxt = _M_before_begin._M_nxt;1465 _M_before_begin._M_nxt = __node;1466 if (__node->_M_nxt)1467 // We must update former begin bucket that is pointing to1468 // _M_before_begin.1469 _M_buckets[_M_bucket_index(__node->_M_next())] = __node;1470 _M_buckets[__bkt] = &_M_before_begin;1471 }1472 }
至此,插入完毕。
begin end size
结合图例,很容易可以给出begin, end和size的实现:
368 __node_type*369 _M_begin() const370 { return static_cast<__node_type*>(_M_before_begin._M_nxt); } //返回哨兵节点的next指针481 iterator482 begin() noexcept483 { return iterator(_M_begin()); }489 iterator490 end() noexcept491 { return iterator(nullptr); }505 size_type506 size() const noexcept507 { return _M_element_count; }
其他成员函数
只要理解了Hashtable的结构,其他成员函数的代码还是比较self-explained的,这里就暂且略过。