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.