Comment combiner les valeurs de hachage en C ++ 0x?

87

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

Je n'ai pas trouvé de hash_combinefonction cependant, comme présenté en boost . Quelle est la manière la plus propre de mettre en œuvre quelque chose comme ça? Peut-être, en utilisant C ++ 0x xor_combine?

Neil G
la source

Réponses:

93

Eh bien, faites-le comme les gars du boost l'ont fait:

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
la source
26
ouais, c'est le mieux que je puisse faire aussi. Je ne comprends pas comment le comité des normes a refusé quelque chose d'aussi évident.
Neil G
13
@Neil: Je suis d'accord. Je pense qu'une solution simple pour eux serait l'exigence de la bibliothèque d'avoir un hachage pour std::pair(ou tuple, même). Il calculerait le hachage de chaque élément, puis les combinerait. (Et dans l'esprit de la bibliothèque standard, d'une manière définie par l'implémentation.)
GManNickG
3
Il y a beaucoup de choses évidentes omises de la norme. Le processus d'examen intensif par les pairs rend difficile la sortie de ces petites choses.
stinky472
15
Pourquoi ces nombres magiques ici? Et ce qui précède n'est-il pas dépendant de la machine (par exemple, ne sera-t-il pas différent sur les plates-formes x86 et x64)?
einpoklum
3
Il y a un article suggérant l'inclusion de hash_combine ici
SSJ_GZ
36

Je vais le partager ici car il peut être utile à d'autres personnes à la recherche de cette solution: à partir de la réponse @KarlvonMoor , voici une version de modèle variadique, qui est plus courte dans son utilisation si vous devez combiner plusieurs valeurs ensemble:

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

Usage:

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

Cela a été écrit à l'origine pour implémenter une macro variadique afin de rendre facilement les types personnalisés hachables (ce qui, je pense, est l'un des principaux usages d'une hash_combinefonction):

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

Usage:

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
la source
Pourquoi la graine est-elle toujours décalée par bits de 6 et 2, respectivement?
j00hi
5

Cela pourrait également être résolu en utilisant un modèle variadique comme suit:

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

Usage:

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

On pourrait certainement créer une fonction de modèle, mais cela pourrait entraîner une déduction de type désagréable, par exemple hash("Hallo World!")calculera une valeur de hachage sur le pointeur plutôt que sur la chaîne. C'est probablement la raison pour laquelle le standard utilise un struct.

kiloalphaindia
la source
4

Il y a quelques jours, j'ai proposé une version légèrement améliorée de cette réponse (le support C ++ 17 est requis):

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

Le code ci-dessus est meilleur en termes de génération de code. J'ai utilisé la fonction qHash de Qt dans mon code, mais il est également possible d'utiliser d'autres hachages.

vt4a2h
la source
Écrivez l'expression de repli comme (int[]){0, (hashCombine(seed, rest), 0)...};et cela fonctionnera également en C ++ 11.
Henri Menke
3

J'aime vraiment l'approche C ++ 17 du réponse de vt4a2h , mais elle souffre d'un problème:Rest est passé par valeur alors qu'il serait plus souhaitable de les transmettre par des références const (ce qui est un must s'il doit être utilisable avec les types de déplacement uniquement).

Voici la version adaptée qui utilise toujours une expression de pli (qui est la raison pour laquelle elle nécessite C ++ 17 ou supérieur) et utilise std::hash(au lieu de la fonction de hachage 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), ...);
}

Par souci d'exhaustivité: tous les types utilisables avec cette version de hash_combinedoivent avoir un spécialisation de modèle pour être hashinjectés dans lestd espace de noms.

Exemple:

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

Donc, ce type Bdans l'exemple ci-dessus est également utilisable dans un autre type A, comme le montre l'exemple d'utilisation suivant:

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
la source
À mon avis, il est plus agréable d'utiliser les Hasharguments de modèle des conteneurs standard pour spécifier votre hachage personnalisé au lieu de l'injecter dans l' stdespace de noms.
Henri Menke
3

La réponse de vt4a2h est certainement agréable mais utilise l'expression C ++ 17 fold et tout le monde n'est pas capable de passer facilement à une nouvelle chaîne d'outils. La version ci-dessous utilise l'astuce de l'expandeur pour émuler une expression fold et fonctionne en C ++ 11 et C ++ 14 .

De plus, j'ai marqué la fonction inlineet j'utilise une transmission parfaite pour les arguments du modèle variadique.

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

Exemple en direct sur l'Explorateur de compilateurs

Henri Menke
la source
Ça a l'air beaucoup mieux, merci! Je ne me souciais probablement pas de passer par valeur, car j'utilisais des objets partagés implicitement, par exemple, comme QString.
vt4a2h