Última atualização:2026-10-12
STL hashtable tabela hash¶
0. Introdução¶
A tabela hash serve de contêiner subjacente para unordered_map, unordered_set e afins; a partir do gcc 2.9 a quantidade de código-fonte cresceu muito!
O código lido desta vez ainda é o do gcc 4.9.1; a quantidade de código é enorme, então não vamos desdobrar tudo — o foco é estudar a arte e a técnica da tabela hash subjacente. Essas duas palavras até rimam, haha; entremos no texto~
1. Primeiro contato com o Hashtable¶
Primeiro, uma olhada no código-fonte do Hashtable:
template<typename _Key, typename _Value, typename _Alloc,
typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash,
typename _RehashPolicy, typename _Traits>
class _Hashtable
: public __detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal,
public __detail::_Map_base<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>,
public __detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>,
public __detail::_Rehash_base<_Key, _Value, _Alloc, _ExtractKey, _Equal,_H1, _H2, _Hash, _RehashPolicy, _Traits>,
public __detail::_Equality<_Key, _Value, _Alloc, _ExtractKey, _Equal,_H1, _H2, _Hash, _RehashPolicy, _Traits>,
private __detail::_Hashtable_alloc<typename __alloctr_rebind<_Alloc,__detail::_Hash_node<_Value,_Traits::__hash_cached::value> >::__type>
{
};
Quem nunca estudou class templates fica totalmente perdido — só de contar os parâmetros de template já dá tontura...
E ainda a herança: de repente aparecem tantos pais...
A seguir, analisaremos os seus pais um a um e depois voltaremos à tabela hash.
2. _Hashtable_base¶
O comentário diz o seguinte:
Helper class adding management of _Equal functor to _Hash_code_base type.
Classe auxiliar que adiciona o gerenciamento do functor _Equal ao tipo _Hash_code_base.
Comparando o código, já dá para entender o que isso significa:
template<typename _Key, typename _Value,
typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash, typename _Traits>
struct _Hashtable_base
: public _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, _Traits::__hash_cached::value>,
private _Hashtable_ebo_helper<0, _Equal>
{
};
_Hash_code_base com _Hashtable_base, a diferença entre os dois é apenas um _Equal; com isso, essa frase está explicada.
A sua classe base, por sua vez, tem duas:
Continuemos a rastrear essas duas classes!
2.1 _Hash_code_base¶
O último parâmetro desta classe, __cache_hash_code, indica se o hash code é cacheado.
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash,bool __cache_hash_code>
struct _Hash_code_base;
Conforme o caching ou não, obtêm-se as versões de especialização parcial:
- usa range hashing (na verdade, o que costumamos chamar de método da divisão com resto), sem cachear o hash code.
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, false>
: private _Hashtable_ebo_helper<0, _ExtractKey>,
private _Hashtable_ebo_helper<1, _Hash>
}
Para esta especialização parcial, o caching é desnecessário, de modo que no código há apenas a declaração, sem definição!
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, _Hash, true>;
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2,
_Default_ranged_hash, false>
: private _Hashtable_ebo_helper<0, _ExtractKey>,
private _Hashtable_ebo_helper<1, _H1>,
private _Hashtable_ebo_helper<2, _H2>
{
};
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2>
struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2,
_Default_ranged_hash, true>
: private _Hashtable_ebo_helper<0, _ExtractKey>,
private _Hashtable_ebo_helper<1, _H1>,
private _Hashtable_ebo_helper<2, _H2>
{
Certamente os _H1 e _H2 acima deixam todos confusos; vejamos:
(1)Default range hashing function (função padrão de range hashing)
E esta é a seguinte: Os detalhes podem ser vistos na explicação mais adiante.struct _Mod_range_hashing
{
typedef std::size_t first_argument_type;
typedef std::size_t second_argument_type;
typedef std::size_t result_type;
result_type
operator()(first_argument_type __num,
second_argument_type __den) const noexcept
{ return __num % __den; }
};
Correspondendo isso à estrutura de dados da tabela hash, temos a função de dispersão que todos conhecem: o método da divisão com resto.
Em segundo lugar, há o_Default_ranged_hash:
Este serve apenas como marcação da range hash function (Default ranged hash function) já calculada por padrão:
Assim, fica evidente a função de dispersão da tabela hash subjacente: por padrão, é exatamente esta.
E a marcação recém-mencionada existe porque combinar os objetos dos tipos H1 e H2 em H consumiria operações de cópia adicionais; por isso essa marcação foi introduzida.
Com isso, encerra-se a explicação de _H1 e _H2 mencionados acima: correspondem, respectivamente, às duas funções acima.
(2)Operação de rehash
Em seguida, há outra operação importante chamada rehash; certamente todos conhecem bem o rehash: quando as colisões da tabela de dispersão atingem determinado nível, é preciso recolocar as keys em posições adequadas — e é exatamente isso que o código-fonte subjacente da tabela hash faz, aqui encapsulado em uma rehash policy:
A operação de rehash menciona: o tamanho do bucket (bucket size) por padrão costuma ser o menor primo, garantindo assim que o load factor (razão entre a quantidade atual de elementos do contêiner e o número de buckets) fique suficientemente pequeno. O load factor mede o quão cheia está a tabela hash; o valor padrão do fator de carga máximo é 1.0._Prime_rehash_policy(float __z = 1.0)
: _M_max_load_factor(__z), _M_next_resize(0) { }
> rehash计算下一个素数桶
inline std::size_t
_Prime_rehash_policy::
_M_next_bkt(std::size_t __n) const
{
const unsigned long* __p = std::lower_bound(__prime_list, __prime_list + _S_n_primes, __n);
_M_next_resize =
static_cast<std::size_t>(__builtin_ceil(*__p * _M_max_load_factor));
return *__p;
}
Tabela de primos
Como procurar os primos?
Vimos acima que há uma __prime_list; fomos procurar e, em libstdc++v3/src/shared/hashtable-aux.cc, encontramos a tabela completa de primos.
Ao todo são 256+1+49 ou 256+49.
Se sizeof(unsigned long)!=8, são 256+1+49; caso contrário, 256+49.
extern const unsigned long __prime_list[] = // 256 + 1 or 256 + 48 + 1
{
2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,
37ul, 41ul, 43ul, 47ul, 53ul, 59ul, 61ul, 67ul, 71ul, 73ul, 79ul,
83ul, 89ul, 97ul, 103ul, 109ul, 113ul, 127ul, 137ul, 139ul, 149ul,
// 后面还有很多
}
Calcular o bucket correspondente a um elemento
Calcula-se o menor bucket a partir do fator de carga máximo e, em seguida, a partir do bucket, calcula-se o menor bucket primo correspondente a cada elemento.
inline std::size_t
_Prime_rehash_policy::
_M_bkt_for_elements(std::size_t __n) const
{
// 获取最小的桶
const float __min_bkts = __n / _M_max_load_factor;
// 获取最小素数p
const unsigned long* __p = std::lower_bound(__prime_list, __prime_list
+ _S_n_primes, __min_bkts);
_M_next_resize =
static_cast<std::size_t>(__builtin_ceil(*__p * _M_max_load_factor));
return *__p;
}
O _Hashtable_ebo_helper é justamente o EBO (classe base vazia) que aprendemos antes
O _Map_base implementa, principalmente por especialização parcial, a sobrecarga dos operadores [] e at.
`_Insert` completa principalmente o que diz respeito à inserção.
`_Rehash_base` completa principalmente a passagem do valor do fator de carga máximo no rehash acima.
`_Equality_base` fornece principalmente os tipos e funções comuns para a classe _Equality.
Até aqui, todas as classes herdadas pelo _Hashtable acima foram explicadas.
2. Estrutura dos nós da lista ligada na hashtable¶
A classe base dos hash nodes contém apenas declarações de ponteiro.
struct _Hash_node_base
{
_Hash_node_base* _M_nxt;
_Hash_node_base() noexcept : _M_nxt() { }
_Hash_node_base(_Hash_node_base* __next) noexcept : _M_nxt(__next) { }
};
template<typename _Value>
struct _Hash_node_value_base : _Hash_node_base
{
typedef _Value value_type;
__gnu_cxx::__aligned_buffer<_Value> _M_storage;
_Value*
_M_valptr() noexcept
{ return _M_storage._M_ptr(); }
const _Value*
_M_valptr() const noexcept
{ return _M_storage._M_ptr(); }
_Value&
_M_v() noexcept
{ return *_M_valptr(); }
const _Value&
_M_v() const noexcept
{ return *_M_valptr(); }
};
Como mencionamos antes se o nó carrega o hash code, o nó precisa trazer o hash code; isso é implementado concretamente abaixo:
/**
* Primary template struct _Hash_node.
*/
template<typename _Value, bool _Cache_hash_code>
struct _Hash_node;
/**
* Specialization for nodes with caches, struct _Hash_node.
*
* Base class is __detail::_Hash_node_value_base.
*/
template<typename _Value>
struct _Hash_node<_Value, true> : _Hash_node_value_base<_Value>
{
std::size_t _M_hash_code;
_Hash_node*
_M_next() const noexcept
{ return static_cast<_Hash_node*>(this->_M_nxt); }
};
/**
* Specialization for nodes without caches, struct _Hash_node.
*
* Base class is __detail::_Hash_node_value_base.
*/
template<typename _Value>
struct _Hash_node<_Value, false> : _Hash_node_value_base<_Value>
{
_Hash_node*
_M_next() const noexcept
{ return static_cast<_Hash_node*>(this->_M_nxt); }
};
Aqui fica claro: os nós dividem-se entre os que contêm hash code e os que não contêm; conforme o parâmetro de template passado, é chamada a versão de especialização parcial correspondente.
3. Iteradores¶
A classe base dos iteradores usa explicitamente a sintaxe using, semelhante ao typedef: depois de definida, pode-se usar diretamente a sintaxe __node_type nas definições; a função _M_incr obtém o próximo nó da lista ligada.
/// Base class for node iterators.
template<typename _Value, bool _Cache_hash_code>
struct _Node_iterator_base
{
using __node_type = _Hash_node<_Value, _Cache_hash_code>;
__node_type* _M_cur;
_Node_iterator_base(__node_type* __p) noexcept
: _M_cur(__p) { }
void
_M_incr() noexcept
{ _M_cur = _M_cur->_M_next(); }
};
Iterador de nós: estudando o código abaixo, aprendem-se duas coisas: - primeiro: o uso de using - o iterador da hashtable é um forward_iterator - sobrecarrega os quatro operadores ++, --, *, ->
template<typename _Value, bool __constant_iterators, bool __cache>
struct _Node_iterator
: public _Node_iterator_base<_Value, __cache>
{
private:
using __base_type = _Node_iterator_base<_Value, __cache>;
using __node_type = typename __base_type::__node_type;
public:
typedef _Value value_type;
typedef std::ptrdiff_t difference_type;
typedef std::forward_iterator_tag iterator_category;
using pointer = typename std::conditional<__constant_iterators,
const _Value*, _Value*>::type;
using reference = typename std::conditional<__constant_iterators,
const _Value&, _Value&>::type;
_Node_iterator() noexcept
: __base_type(0) { }
explicit
_Node_iterator(__node_type* __p) noexcept
: __base_type(__p) { }
reference
operator*() const noexcept
{ return this->_M_cur->_M_v(); }
pointer
operator->() const noexcept
{ return this->_M_cur->_M_valptr(); }
_Node_iterator&
operator++() noexcept
{
this->_M_incr();
return *this;
}
_Node_iterator
operator++(int) noexcept
{
_Node_iterator __tmp(*this);
this->_M_incr();
return __tmp;
}
};
4. Estudo aprofundado das estruturas internas importantes da hashtable¶
A estrutura interna mantém uma lista ligada simples em cada elemento e, em seguida, executa na lista as operações de inserção, busca, remoção etc. de elementos; cada elemento é chamado de bucket. A construção subjacente primeiro usa H1 para calcular o hash code da key e, depois, por meio do método da divisão com resto H2, obtém o bucket correspondente.
template<typename _Key, typename _Value, typename _Alloc,
typename _ExtractKey, typename _Equal,
typename _H1, typename _H2, typename _Hash,
typename _RehashPolicy, typename _Traits>
class _Hashtable
private:
__bucket_type* _M_buckets; //_ Hash_node_base *
size_type _M_bucket_count; // bucket 节点个数
__node_base _M_before_begin; // _NodeAlloc::value_type
size_type _M_element_count; // //hashtable中list节点个数
_RehashPolicy _M_rehash_policy; // rehash策略
__bucket_type _M_single_bucket; // 只需要一个桶用
};
Função begin
Chama _M_begin:
Pode-se imaginar _M_before_begin como um nó head; o primeiro nó é o próximo dele.
Função end
Por ser uma lista ligada simples, basta retornar o último.
Funções size e empty
size_type
size() const noexcept
{ return _M_element_count; }
bool
empty() const noexcept
{ return size() == 0; }
Quantidade de buckets
Cálculo do fator de carga
Quantidade atual de elementos dividida pela quantidade de buckets
float
load_factor() const noexcept
{
return static_cast<float>(size()) / static_cast<float>(bucket_count());
}
Cálculo do índice do bucket
Obtém o índice do bucket a partir da key passada.
size_type
bucket(const key_type& __k) const
{ return _M_bucket_index(__k, this->_M_hash_code(__k)); }
_Hash_code_base há a seguinte implementação:
O _M_h1 retorna _H1; quando não se sabe o que é _H1, podemos encontrar em unordered_map que _Hash=hash<_Key>; portanto, a função abaixo é a expressão matemática:
h1(k) para obter o hash code.
Retorna o hash code do bucket.
Retorna o índice do bucket.
A definição de _M_bucket_index é encontrada mais adiante no mesmo arquivo:
size_type
_M_bucket_index(const key_type& __k, __hash_code __c) const
{ return __hash_code_base::_M_bucket_index(__k, __c, _M_bucket_count); }
_M_bucket_index em __hash_code_base, e o encontramos em bits/hashtable_policy.h:
std::size_t
_M_bucket_index(const _Key&, __hash_code __c,
std::size_t __n) const
{ return _M_h2()(__c, __n); }
unordered_map que _H2 usa por padrão _Mod_range_hashing; olhemos este código-fonte:
struct _Mod_range_hashing
{
typedef std::size_t first_argument_type;
typedef std::size_t second_argument_type;
typedef std::size_t result_type;
result_type
operator()(first_argument_type __num,
second_argument_type __den) const noexcept
{ return __num % __den; }
};
h2(c,n).
Logo, a expressão matemática correspondente à obtenção do índice do bucket pelo bucket acima é:
Tópicos relacionados¶
- Análise do código-fonte do STL array | C++那些事
- Análise aprofundada do STL string | C++那些事
- Questões de entrevista sobre STL string explicadas | C++那些事
- Análise do código-fonte do STL deque | C++那些事
- STL otimização de classe base vazia EBO | C++那些事
- STL implementação manual de tabela hash | C++那些事
- Análise do código-fonte dos iteradores da STL | C++那些事
- Análise do código-fonte do STL list | C++那些事
- STL map e multimap | C++那些事
- Análise do código-fonte do STL queue e stack | C++那些事