Última actualización:2026-10-12
STL hashtable tabla hash¶
0. Introducción¶
La tabla hash es el contenedor subyacente de unordered_map, unordered_set y similares; ¡a partir de gcc2.9 la cantidad de código fuente se disparó!
El código leído esta vez sigue siendo el de gcc4.9.1; hay muchísimo código, así que no lo desplegaremos todo: nos centraremos en el arte y la técnica de la tabla hash subyacente. Estas dos palabras suenan bien juntas, jeje. ¡Vamos al grano!
1. Primer contacto con Hashtable¶
Echemos primero un vistazo al código fuente de 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>
{
};
Quien no haya estudiado plantillas de clase se queda totalmente perdido: solo de contar los parámetros de plantilla ya mareas...
Y luego su herencia: de golpe saca tantos padres...
A continuación analizaremos uno a uno sus padres y luego volveremos a la tabla hash.
2._Hashtable_base¶
En los comentarios dice lo siguiente:
Helper class adding management of _Equal functor to _Hash_code_base type.
Clase auxiliar que añade la gestión del functor _Equal al tipo _Hash_code_base.
Comparando el código se ve enseguida qué 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 con _Hashtable_base, la única diferencia es un _Equal; con esto queda explicada la frase.
Sus clases base son dos:
¡Sigamos rastreando estas dos clases!
2.1 _Hash_code_base¶
El último parámetro de esta clase, __cache_hash_code, indica si se cachea el hash code.
template<typename _Key, typename _Value, typename _ExtractKey,
typename _H1, typename _H2, typename _Hash,bool __cache_hash_code>
struct _Hash_code_base;
Según se cachee o no, se obtienen sus versiones especializadas parcialmente:
- Usa hash por rangos (en realidad, lo que normalmente llamamos método de la división), sin cachear el hash code.
- Usa hash por rangos (en realidad, lo que normalmente llamamos método de la división), cacheando el hash code.
Para esta especialización parcial, la caché no es necesaria, ¡así que en el código solo está la declaración y no la definición!
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>
{
Seguro que _H1 y _H2 anteriores resultan confusos; veámoslos:
(1) Default range hashing function (función hash por rangos por defecto)
La siguiente es justo esto: Más adelante se puede ver la explicación concreta.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; }
};
Lo anterior, llevado a la estructura de datos de la tabla hash, es la función de dispersión que todos conocéis: el método de la división.
A continuación,_Default_ranged_hash:
Esto solo sirve como marcador: la función hash por rangos ya calculada por defecto (Default ranged hash function):
Así que hasta aquí la función de dispersión de la tabla hash subyacente queda clara: por defecto es exactamente esta.
Y el marcador mencionado hace un momento existe porque componer objetos de los tipos H1 y H2 en H consumiría operaciones de copia adicionales; de ahí la introducción de este marcador.
Con esto termina la explicación de _H1 y _H2 mencionados arriba: se corresponden, respectivamente, con las dos funciones anteriores.
(2) Operación rehash
Acto seguido hay otra pieza importante llamada rehash. Seguro que conocéis bien rehash: cuando los conflictos de la tabla de dispersión alcanzan cierto nivel, hay que recolocar las keys en posiciones adecuadas, y eso es precisamente lo que hace el código fuente de la tabla hash; aquí se encapsula en una rehash policy:
Sobre la operación rehash se comenta: el tamaño de cubo (bucket size) por defecto suele ser el primo más pequeño, para garantizar que el factor de carga (load factor, el cociente entre el número actual de elementos del contenedor y el número de cubos) sea suficientemente pequeño. El factor de carga mide el grado de llenado de la tabla hash; el factor de carga máximo por defecto es 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;
}
Tabla de primos
¿Cómo se buscan los primos?
Arriba aparece un __prime_list, así que fuimos a buscarlo y en libstdc++v3/src/shared/hashtable-aux.cc encontramos la tabla completa de primos.
Dentro hay 256+1+49 o 256+49 en total.
Si sizeof(unsigned long)!=8 son 256+1+49; en caso contrario, 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,
// 后面还有很多
}
Cálculo del cubo correspondiente a un elemento
A partir del factor de carga máximo se calcula el número mínimo de cubos y, a partir de este, el primo mínimo de cubos correspondiente 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;
}
_Hashtable_ebo_helper es el EBO (clase base vacía) que ya estudiamos antes.
_Map_base implementa, mediante especialización parcial, la sobrecarga de los operadores [] y at.
`_Insert` se encarga principalmente de todo lo relacionado con la inserción.
`_Rehash_base` se encarga principalmente de transmitir el valor del factor de carga máximo del rehash descrito arriba.
`_Equality_base` proporciona principalmente tipos y funciones comunes para la clase _Equality.
Hasta aquí quedan explicadas todas las clases de las que hereda el _Hashtable anterior.
2. Estructura de los nodos de la lista enlazada en hashtable¶
Clase base del hash node: contiene únicamente la declaración del puntero.
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(); }
};
/**
* 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); }
};
Aquí queda claro: los nodos se dividen en los que incluyen hash code y los que no; en concreto, según el parámetro de plantilla que se pase, se invoca la versión especializada correspondiente.
3. Iteradores¶
La clase base del iterador muestra el uso de la sintaxis using, similar a typedef, de modo que después se puede definir directamente con la sintaxis __node_type; la función _M_incr obtiene el siguiente nodo de la lista enlazada.
/// 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 nodo: estudiando el código siguiente se aprenden dos cosas: - Primero: el uso de using - el iterador de hashtable es un forward_iterator - sobrecarga los cuatro operadores ++, --, * y ->
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. Estudio en profundidad de la importante estructura interna de hashtable¶
La estructura interna mantiene una lista enlazada simple en cada elemento y, sobre esa lista, ejecuta las operaciones de inserción, búsqueda y borrado de elementos; cada elemento se denomina cubo (bucket). La construcción subyacente primero usa H1 para calcular el hash code de la key y después obtiene el cubo correspondiente mediante el método de la división H2.
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; // 只需要一个桶用
};
Función begin
Invoca _M_begin:
Puedes imaginar _M_before_begin como un nodo head: el primer nodo es el siguiente nodo.
Función end
Al ser una lista simplemente enlazada, basta con devolver el último.
Funciones size y empty
size_type size() const noexcept { return _M_element_count; } bool empty() const noexcept { return size() == 0; }Número de cubos
Cálculo del factor de carga
El número actual de elementos dividido por el número de cubos
float
load_factor() const noexcept
{
return static_cast<float>(size()) / static_cast<float>(bucket_count());
}
Cálculo del índice del cubo
Obtiene el índice del cubo a partir de la key que se le pasa.
size_type
bucket(const key_type& __k) const
{ return _M_bucket_index(__k, this->_M_hash_code(__k)); }
_Hash_code_base hay la implementación siguiente:
_M_h1 devuelve _H1; cuando no sepamos qué es _H1, podemos buscarlo en unordered_map, donde encontramos _Hash=hash<_Key>, por lo que la función siguiente es la expresión matemática:
h1(k) para obtener el hash code.
Devuelve el hash code del cubo.
Devuelve el índice del cubo.
La definición de _M_bucket_index se encuentra más adelante en el mismo archivo:
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 en __hash_code_base y lo encontramos en 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 se encuentra que _H2 usa por defecto _Mod_range_hashing; veamos este código fuente:
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).
Por tanto, la expresión matemática correspondiente a la obtención del índice del cubo con bucket anterior es:
Temas relacionados¶
- Análisis del código fuente de STL array | C++那些事
- Análisis profundo de STL string | C++那些事
- STL string preguntas de entrevista explicadas | C++那些事
- Análisis del código fuente de STL deque | C++那些事
- STL optimización de clase base vacía EBO | C++那些事
- STL implementación de una tabla hash escrita a mano | C++那些事
- Análisis del código fuente de los iteradores STL | C++那些事
- Análisis del código fuente de STL list | C++那些事
- STL map y multimap | C++那些事
- Análisis del código fuente de STL queue y stack | C++那些事