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?

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

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

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.


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.


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

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


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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.