¿Cómo combino valores hash en C ++ 0x?

87

C ++ 0x agrega hash<...>(...).

Sin embargo, no pude encontrar una hash_combinefunción, como se presenta en boost . ¿Cuál es la forma más limpia de implementar algo como esto? ¿Quizás, usando C ++ 0x xor_combine?

Neil G
fuente

Respuestas:

93

Bueno, hazlo como lo hicieron los chicos de impulso:

template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
Karl von Moor
fuente
26
sí, eso es lo mejor que pude hacer también. No entiendo cómo el comité de estándares rechazó algo tan obvio.
Neil G
13
@Neil: Estoy de acuerdo. Creo que una solución simple para ellos sería el requisito de que la biblioteca tenga un hash para std::pair(o tupleincluso). Calcularía el hash de cada elemento y luego los combinaría. (Y en el espíritu de la biblioteca estándar, de una manera definida por la implementación.)
GManNickG
3
Hay muchas cosas obvias omitidas del estándar. El proceso de revisión intensiva por pares hace que sea difícil sacar esas pequeñas cosas por la puerta.
stinky472
15
¿Por qué estos números mágicos aquí? ¿Y no es lo anterior dependiente de la máquina (por ejemplo, no será diferente en las plataformas x86 y x64)?
einpoklum
3
Hay un documento que sugiere la inclusión de hash_combine aquí
SSJ_GZ
35

Lo compartiré aquí, ya que puede ser útil para otras personas que buscan esta solución: a partir de la respuesta de @KarlvonMoor , aquí hay una versión de plantilla variada, que es más tersa en su uso si tiene que combinar varios valores:

inline void hash_combine(std::size_t& seed) { }

template <typename T, typename... Rest>
inline void hash_combine(std::size_t& seed, const T& v, Rest... rest) {
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
    hash_combine(seed, rest...);
}

Uso:

std::size_t h=0;
hash_combine(h, obj1, obj2, obj3);

Esto se escribió originalmente para implementar una macro variadic para hacer fácilmente los tipos personalizados hash (que creo que es uno de los usos principales de una hash_combinefunción):

#define MAKE_HASHABLE(type, ...) \
    namespace std {\
        template<> struct hash<type> {\
            std::size_t operator()(const type &t) const {\
                std::size_t ret = 0;\
                hash_combine(ret, __VA_ARGS__);\
                return ret;\
            }\
        };\
    }

Uso:

struct SomeHashKey {
    std::string key1;
    std::string key2;
    bool key3;
};

MAKE_HASHABLE(SomeHashKey, t.key1, t.key2, t.key3)
// now you can use SomeHashKey as key of an std::unordered_map
Matteo Italia
fuente
¿Por qué la semilla siempre se desplaza en bits en 6 y 2, respectivamente?
j00hi
5

Esto también podría resolverse utilizando una plantilla variada de la siguiente manera:

#include <functional>

template <typename...> struct hash;

template<typename T> 
struct hash<T> 
    : public std::hash<T>
{
    using std::hash<T>::hash;
};


template <typename T, typename... Rest>
struct hash<T, Rest...>
{
    inline std::size_t operator()(const T& v, const Rest&... rest) {
        std::size_t seed = hash<Rest...>{}(rest...);
        seed ^= hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
        return seed;
    }
};

Uso:

#include <string>

int main(int,char**)
{
    hash<int, float, double, std::string> hasher;
    std::size_t h = hasher(1, 0.2f, 2.0, "Hello World!");
}

Ciertamente se podría hacer una función de plantilla, pero esto podría causar alguna deducción de tipo desagradable, por ejemplo hash("Hallo World!"), calculará un valor hash en el puntero en lugar de en la cadena. Esta es probablemente la razón por la que el estándar usa una estructura.

kiloalfaindia
fuente
4

Hace unos días se me ocurrió una versión ligeramente mejorada de esta respuesta (se requiere compatibilidad con C ++ 17):

template <typename T, typename... Rest>
void hashCombine(uint& seed, const T& v, Rest... rest)
{
    seed ^= ::qHash(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    (hashCombine(seed, rest), ...);
}

El código anterior es mejor en términos de generación de código. Usé la función qHash de Qt en mi código, pero también es posible usar cualquier otro hash.

vt4a2h
fuente
Escriba la expresión de pliegue como (int[]){0, (hashCombine(seed, rest), 0)...};y también funcionará en C ++ 11.
Henri Menke
3

Realmente me gusta el enfoque de C ++ 17 de la respuesta de vt4a2h , sin embargo, tiene un problema: Restse transmite por valor, mientras que sería más deseable transmitirlos mediante referencias constantes (que es imprescindible si debe ser utilizable con tipos de solo movimiento).

Aquí está la versión adaptada que todavía usa una expresión de plegado (que es la razón por la que requiere C ++ 17 o superior) y usa std::hash(en lugar de la función hash Qt):

template <typename T, typename... Rest>
void hash_combine(std::size_t& seed, const T& v, const Rest&... rest)
{
    seed ^= std::hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    (hash_combine(seed, rest), ...);
}

En aras de la integridad: todos los tipos que se podrán utilizar con esta versión de hash_combinedeben tener una especialización de plantilla para hashinyectarse en el stdespacio de nombres.

Ejemplo:

namespace std // Inject hash for B into std::
{
    template<> struct hash<B>
    {
        std::size_t operator()(B const& b) const noexcept
        {
            std::size_t h = 0;
            cgb::hash_combine(h, b.firstMember, b.secondMember, b.andSoOn);
            return h;
        }
    };
}

Entonces, ese tipo Ben el ejemplo anterior también se puede usar dentro de otro tipo A, como muestra el siguiente ejemplo de uso:

struct A
{
    std::string mString;
    int mInt;
    B mB;
    B* mPointer;
}

namespace std // Inject hash for A into std::
{
    template<> struct hash<A>
    {
        std::size_t operator()(A const& a) const noexcept
        {
            std::size_t h = 0;
            cgb::hash_combine(h,
                a.mString,
                a.mInt,
                a.mB, // calls the template specialization from above for B
                a.mPointer // does not call the template specialization but one for pointers from the standard template library
            );
            return h;
        }
    };
}
j00hi
fuente
En mi opinión, es mejor usar los Hashargumentos de la plantilla de los contenedores estándar para especificar su hash personalizado en lugar de inyectarlo en el stdespacio de nombres.
Henri Menke
3

La respuesta de vt4a2h es ciertamente agradable, pero usa la expresión C ++ 17 veces y no todos pueden cambiar fácilmente a una cadena de herramientas más nueva. La siguiente versión utiliza el truco de expansión para emular una expresión de plegado y también funciona en C ++ 11 y C ++ 14 .

Además, inlinemarqué la función y utilicé el reenvío perfecto para los argumentos de la plantilla variadic.

template <typename T, typename... Rest>
inline void hashCombine(std::size_t &seed, T const &v, Rest &&... rest) {
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    (int[]){0, (hashCombine(seed, std::forward<Rest>(rest)), 0)...};
}

Ejemplo en vivo en Compiler Explorer

Henri Menke
fuente
¡Se ve mucho mejor, gracias! Probablemente no me importaba pasar por valor, porque usé algunos objetos compartidos implícitamente, por ejemplo, como QString.
vt4a2h