Come combino i valori hash in C ++ 0x?


87

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

hash_combineTuttavia, non sono riuscito a trovare una funzione, come presentato in boost . Qual è il modo più pulito per implementare qualcosa di simile? Forse, usando C ++ 0x xor_combine?

Risposte:


94

Bene, fallo come hanno fatto i ragazzi del boost:

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);
}

27
sì, è il meglio che potrei fare anch'io. Non capisco come il comitato per gli standard abbia rifiutato qualcosa di così ovvio.
Neil G

13
@ Neil: sono d'accordo. Penso che una soluzione semplice per loro sarebbe il requisito della libreria di avere un hash per std::pair(o tuple, anche). Calcolerebbe l'hash di ogni elemento, quindi li combina. (E nello spirito della libreria standard, in un modo definito di implementazione.)
GManNickG

3
Ci sono molte cose ovvie omesse dallo standard. Il processo di peer review intensivo rende difficile portare fuori queste piccole cose.
stinky472

15
Perché questi numeri magici qui? E quanto sopra non dipende dalla macchina (ad esempio, non sarà diverso sulle piattaforme x86 e x64)?
einpoklum

3
C'è un documento che suggerisce l'inclusione di hash_combine qui
SSJ_GZ

37

Lo condividerò qui poiché può essere utile ad altri che cercano questa soluzione: partendo dalla risposta di @KarlvonMoor , ecco una versione del modello variadico, che è più tesa nel suo utilizzo se devi combinare più valori insieme:

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...);
}

Utilizzo:

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

Questo è stato scritto originariamente per implementare una macro variadica per rendere facilmente hashable i tipi personalizzati (che penso sia uno degli usi primari di una hash_combinefunzione):

#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;\
            }\
        };\
    }

Utilizzo:

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

Perché il seme viene sempre spostato di bit rispettivamente di 6 e 2?
j00hi

5

Questo potrebbe anche essere risolto utilizzando un modello variadico come segue:

#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;
    }
};

Utilizzo:

#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!");
}

Si potrebbe certamente creare una funzione modello, ma ciò potrebbe causare una sgradevole deduzione di tipo, ad esempio hash("Hallo World!")calcolerà un valore hash sul puntatore piuttosto che sulla stringa. Questo è probabilmente il motivo per cui lo standard utilizza una struttura.


5

Qualche giorno fa ho trovato una versione leggermente migliorata di questa risposta (è richiesto il supporto 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), ...);
}

Il codice sopra è migliore in termini di generazione del codice. Ho usato la funzione qHash di Qt nel mio codice, ma è anche possibile utilizzare qualsiasi altro hash.


Scrivi l'espressione di piegatura come (int[]){0, (hashCombine(seed, rest), 0)...};e funzionerà anche in C ++ 11.
Henri Menke

3

Mi piace molto l'approccio C ++ 17 dalla risposta di vt4a2h , tuttavia soffre di un problema: Restviene trasmesso per valore mentre sarebbe più desiderabile trasmetterli tramite riferimenti const (che è un must se deve essere utilizzabile con i tipi di solo movimento).

Ecco la versione adattata che utilizza ancora un'espressione di piegatura (che è il motivo per cui richiede C ++ 17 o superiore) e utilizza std::hash(invece della funzione 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), ...);
}

Per completezza: tutti i tipi che devono essere utilizzabili con questa versione di hash_combinedevono avere una specializzazione di template da hashinserire nello stdspazio dei nomi.

Esempio:

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;
        }
    };
}

Quindi quel tipo Bnell'esempio sopra è utilizzabile anche all'interno di un altro tipo A, come mostra il seguente esempio di utilizzo:

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;
        }
    };
}

Secondo me è più bello usare gli Hashargomenti del template dei contenitori standard per specificare il tuo hasher personalizzato invece di iniettarlo nello stdspazio dei nomi.
Henri Menke

3

La risposta di vt4a2h è sicuramente carina ma utilizza l'espressione fold C ++ 17 e non tutti sono in grado di passare facilmente a una toolchain più recente. La versione seguente utilizza il trucco dell'espansore per emulare un'espressione di piegatura e funziona anche in C ++ 11 e C ++ 14 .

Inoltre, ho contrassegnato la funzione inlinee utilizzato l'inoltro perfetto per gli argomenti del modello 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)...};
}

Esempio dal vivo su Compiler Explorer


Sembra molto meglio, grazie! Probabilmente non mi interessava passare per valore, perché ho utilizzato alcuni oggetti condivisi implicitamente, ad esempio, come QString.
vt4a2h
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.