Wie kombiniere ich Hashwerte in C ++ 0x?

84

C ++ 0x fügt hinzu hash<...>(...).

Ich konnte jedoch keine hash_combineFunktion finden , wie in Boost dargestellt . Was ist der sauberste Weg, um so etwas zu implementieren? Vielleicht mit C ++ 0x xor_combine?

Neil G.
quelle

Antworten:

90

Nun, mach es einfach so, wie es die Boost-Jungs gemacht haben:

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
quelle
26
Ja, das ist das Beste, was ich auch tun kann. Ich verstehe nicht, wie das Normungsgremium etwas so Offensichtliches abgelehnt hat.
Neil G
13
@Neil: Ich stimme zu. Ich denke, eine einfache Lösung für sie wäre die Anforderung an die Bibliothek, einen Hash für std::pair(oder tuplesogar) zu haben. Es würde den Hash jedes Elements berechnen und sie dann kombinieren. (Und im Geiste der Standardbibliothek, in einer implementierungsdefinierten Weise.)
GManNickG
3
Es gibt viele offensichtliche Dinge, die im Standard weggelassen wurden. Der Prozess der intensiven Begutachtung durch Fachkollegen macht es schwierig, diese kleinen Dinge aus der Tür zu bekommen.
stinky472
13
Warum diese magischen Zahlen hier? Und ist das oben Genannte nicht maschinenabhängig (z. B. auf x86- und x64-Plattformen nicht anders)?
Einpoklum
3
Es gibt ein Papier, das die Aufnahme von hash_combine hier
vorschlägt
34

Ich werde es hier teilen, da es für andere nützlich sein kann, die nach dieser Lösung suchen: Ausgehend von der Antwort von @KarlvonMoor ist hier eine variable Vorlagenversion, die in ihrer Verwendung kürzer ist, wenn Sie mehrere Werte miteinander kombinieren müssen:

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

Verwendung:

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

Dies wurde ursprünglich geschrieben, um ein variadisches Makro zu implementieren, um benutzerdefinierte Typen einfach hashbar zu machen (was meiner Meinung nach eine der Hauptverwendungen einer hash_combineFunktion ist):

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

Verwendung:

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
quelle
Warum ist der Samen immer um 6 bzw. 2 bitverschoben?
J00hi
4

Dies könnte auch durch Verwendung einer variablen Vorlage wie folgt gelöst werden:

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

Verwendung:

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

Man könnte sicherlich eine Vorlagenfunktion erstellen, aber dies könnte zu einem bösen Typabzug führen, z. B. hash("Hallo World!")wird ein Hash-Wert eher auf dem Zeiger als auf der Zeichenfolge berechnet. Dies ist wahrscheinlich der Grund, warum der Standard eine Struktur verwendet.

Kiloalphaindia
quelle
Es gibt keine Spezialisierung für C-Strings. std :: hash <const char *> erzeugt einen Hash des Wertes des Zeigers (der Speicheradresse) und untersucht nicht den Inhalt eines Zeichenarrays. Cref: en.cppreference.com/w/cpp/utility/hash
Matthias
4

Vor einigen Tagen habe ich eine leicht verbesserte Version dieser Antwort gefunden (C ++ 17-Unterstützung ist erforderlich):

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

Der obige Code ist in Bezug auf die Codegenerierung besser. Ich habe die qHash-Funktion von Qt in meinem Code verwendet, aber es ist auch möglich, andere Hascher zu verwenden.

vt4a2h
quelle
Schreiben Sie den Fold-Ausdruck als (int[]){0, (hashCombine(seed, rest), 0)...};und er funktioniert auch in C ++ 11.
Henri Menke
3

Ich mag den C ++ 17-Ansatz aus der Antwort von vt4a2h sehr , aber er hat ein Problem: Der Restwird als Wert weitergegeben, während es wünschenswerter wäre, sie durch konstante Referenzen weiterzugeben (was ein Muss ist, wenn es sein soll verwendbar mit Nur-Verschieben-Typen).

Hier ist die angepasste Version, die immer noch einen Fold-Ausdruck verwendet (weshalb C ++ 17 oder höher erforderlich ist) und verwendet std::hash(anstelle der Qt-Hash-Funktion):

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

Der Vollständigkeit halber: Alle Typen , die mit dieser Version von benutzbar sein soll hash_combinemuss eine haben Template - Spezialisierung für hashinjiziert in den stdNamespace.

Beispiel:

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

Dieser Typ Bim obigen Beispiel kann also auch in einem anderen Typ verwendet werden A, wie das folgende Verwendungsbeispiel zeigt:

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
quelle
Meiner Meinung nach ist es besser, die HashVorlagenargumente der Standardcontainer zu verwenden, um Ihren benutzerdefinierten Hasher anzugeben, als ihn in den stdNamespace einzufügen .
Henri Menke
3

Die Antwort von vt4a2h ist sicherlich nett, verwendet jedoch den C ++ 17-Fold-Ausdruck und nicht jeder kann problemlos zu einer neueren Toolchain wechseln. Die folgende Version verwendet den Expander-Trick, um einen Fold-Ausdruck zu emulieren, und funktioniert auch in C ++ 11 und C ++ 14 .

Zusätzlich habe ich die Funktion markiert inlineund die perfekte Weiterleitung für die verschiedenen Vorlagenargumente verwendet.

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

Live-Beispiel im Compiler Explorer

Henri Menke
quelle
Sieht viel besser aus, danke! Die Übergabe von Werten war mir wahrscheinlich egal, da ich einige implizit gemeinsam genutzte Objekte verwendet habe, z. B. QString.
vt4a2h