Wie spezialisiere ich std :: hash <Key> :: operator () für benutzerdefinierte Typen in ungeordneten Containern?


101

Benutzerdefinierte Schlüsseltypen in zu unterstützen std::unordered_set<Key>und std::unordered_map<Key, Value> man hat zu schaffen operator==(Key, Key)und ein Hash Funktors:

struct X { int id; /* ... */ };
bool operator==(X a, X b) { return a.id == b.id; }

struct MyHash {
  size_t operator()(const X& x) const { return std::hash<int>()(x.id); }
};

std::unordered_set<X, MyHash> s;

Es wäre bequemer, nur std::unordered_set<X> mit einem Standard-Hash für Typ zu schreiben X, wie für Typen, die mit dem Compiler und der Bibliothek geliefert werden. Nach Rücksprache mit

  • C ++ Standard Draft N3242 §20.8.12 [unord.hash] und §17.6.3.4 [hash.requirements],
  • Boost.Unordered
  • g ++ include\c++\4.7.0\bits\functional_hash.h
  • VC10 include\xfunctional
  • verschiedene verwandte Fragen im Stapelüberlauf

es scheint möglich zu spezialisieren std::hash<X>::operator():

namespace std { // argh!
  template <>
  inline size_t 
  hash<X>::operator()(const X& x) const { return hash<int>()(x.id); } // works for MS VC10, but not for g++
  // or
  // hash<X>::operator()(X x) const { return hash<int>()(x.id); }     // works for g++ 4.7, but not for VC10 
}                                                                             

Angesichts der Tatsache, dass die Compiler-Unterstützung für C ++ 11 noch experimentell ist - ich habe Clang nicht ausprobiert -, sind dies meine Fragen:

  1. Ist es legal, dem Namespace eine solche Spezialisierung hinzuzufügen std? Ich habe gemischte Gefühle.

  2. Welche der std::hash<X>::operator()Versionen entspricht gegebenenfalls dem C ++ 11-Standard?

  3. Gibt es eine tragbare Möglichkeit, dies zu tun?


Mit gcc 4.7.2 musste ich einen globalenoperator==(const Key, const Key)
Victor Lyuboslavsky

Beachten Sie, dass die Spezialisierung von std::hash(im Gegensatz zu anderen Dingen im stdNamespace) vom Google Style Guide nicht empfohlen wird. nimm es mit einem Körnchen Salz.
Franklin Yu

Antworten:


128

Es wird ausdrücklich gestattet und empfohlen, dem Namespace * Spezialisierungen hinzuzufügen std. Der richtige (und im Grunde einzige) Weg, eine Hash-Funktion hinzuzufügen, ist folgender:

namespace std {
  template <> struct hash<Foo>
  {
    size_t operator()(const Foo & x) const
    {
      /* your code here, e.g. "return hash<int>()(x.value);" */
    }
  };
}

(Andere beliebte Spezialisierungen, die Sie möglicherweise unterstützen möchten, sind std::less, std::equal_tound std::swap.)

*) Solange einer der beteiligten Typen benutzerdefiniert ist, nehme ich an.


3
Obwohl dies möglich ist, würden Sie generell empfehlen, dies so zu tun? Ich würde unorder_map<eltype, hash, equality>stattdessen die Instanziierung vorziehen , um nicht den Tag eines Menschen mit lustigen ADL-Geschäften zu ruinieren. ( Bearbeiten Sie Pete Beckers Rat zu diesem Thema )
sehe

2
@sehe: Wenn du einen Hash-Funktor herumliegen hast, ist das vielleicht standardmäßig konstruierbar, aber warum? (Gleichheit ist einfacher, da Sie nur member- implementieren würden operator==.) Meine allgemeine Philosophie lautet: Wenn die Funktion natürlich und im Wesentlichen die einzige "richtige" ist (wie der lexikografische Paarvergleich), füge ich sie hinzu std. Wenn es etwas Besonderes ist (wie ein ungeordneter Paarvergleich), dann mache ich es spezifisch für einen Containertyp.
Kerrek SB

3
Ich bin nicht anderer Meinung, aber wo im Standard dürfen und werden wir ermutigt, std Spezialisierungen hinzuzufügen?
Razeh

3
@ Kerrek, ich stimme zu, aber ich hoffte auf ein Kapitel und einen Versverweis auf einen Platz im Standard. Ich habe den Wortlaut gefunden, der die Injektion am 17.6.4.2.1 erlaubt, wo es heißt, dass es nicht erlaubt ist, "sofern nicht anders angegeben", aber ich konnte den Teil "anders angegeben" in der 4000+ Seiten-Spezifikation nicht finden.
Razeh

3
@razeh hier können Sie lesen "Ein Programm kann dem Namespace std nur dann eine Vorlagenspezialisierung für eine Standardbibliotheksvorlage hinzufügen, wenn die Deklaration von einem benutzerdefinierten Typ abhängt und die Spezialisierung die Standardbibliotheksanforderungen für die Originalvorlage erfüllt und nicht ausdrücklich verboten ist . ". Diese Lösung ist also in Ordnung.
Marek R

7

Meine Wette wäre auf das Hash-Vorlagenargument für die Klassen unordered_map / unorder_set / ...:

#include <unordered_set>
#include <functional>

struct X 
{
    int x, y;
    std::size_t gethash() const { return (x*39)^y; }
};

typedef std::unordered_set<X, std::size_t(*)(const X&)> Xunset;
typedef std::unordered_set<X, std::function<std::size_t(const X&)> > Xunset2;

int main()
{
    auto hashX = [](const X&x) { return x.gethash(); };

    Xunset  my_set (0, hashX);
    Xunset2 my_set2(0, hashX); // if you prefer a more flexible set typedef
}

Natürlich

  • hashX könnte genauso gut eine globale statische Funktion sein
  • im zweiten Fall könnten Sie das bestehen
    • das altmodische Funktorobjekt ( struct Xhasher { size_t operator(const X&) const; };)
    • std::hash<X>()
    • jeder Bindungsausdruck, der die Signatur erfüllt -

Ich weiß zu schätzen, dass Sie etwas schreiben können, das keinen Standardkonstruktor hat, aber ich finde immer, dass es eine gewisse Belastung ist, wenn sich jede Kartenkonstruktion an das zusätzliche Argument erinnert - eine ziemlich große Belastung für meinen Geschmack. Ich bin mit einem expliziten Vorlagenargument std::hash
einverstanden

Benutzerdefinierte Typen zur Rettung :-) Im Ernst, ich hoffe, wir würden sie bereits in der Phase, in der ihre Klasse eine enthält, auf die Handgelenke schlagen char*!
Kerrek SB

Hmm ... haben Sie ein Beispiel dafür, wie eine hashSpezialisierung über ADL stört? Ich meine, es ist durchaus plausibel, aber es fällt mir schwer, einen Problemfall zu finden.
Kerrek SB


Es ist alles Spaß und Spiel, bis Sie eine brauchen std::unordered_map<Whatever, Xunset>und es funktioniert nicht, weil Ihr XunsetHasher-Typ nicht standardmäßig konstruierbar ist.
Brian Gordon

4

@ Kerrek SB hat 1) und 3) abgedeckt.

2) Obwohl g ++ und VC10 std::hash<T>::operator()mit unterschiedlichen Signaturen deklarieren , sind beide Bibliotheksimplementierungen Standard-kompatibel.

Der Standard spezifiziert nicht die Mitglieder von std::hash<T>. Es heißt nur, dass jede solche Spezialisierung die gleichen "Hash" -Anforderungen erfüllen muss, die für das zweite Vorlagenargument von std::unordered_setund so weiter erforderlich sind . Nämlich:

  • Der Hash-Typ Hist ein Funktionsobjekt mit mindestens einem Argumenttyp Key.
  • H ist kopierkonstruierbar.
  • H ist zerstörbar.
  • Wenn hes sich um einen Ausdruck vom Typ Hoder const Hhandelt und kein Ausdruck eines Typs ist, der in (möglicherweise const) konvertierbar ist Key, dann h(k)handelt es sich um einen gültigen Ausdruck mit Typ size_t.
  • Wenn hes sich um einen Ausdruck vom Typ Hoder const Hhandelt und uein Wert vom Typ ist Key, h(u)handelt es sich um einen gültigen Ausdruck mit dem Typ, size_tder nicht geändert wird u.

Nein, keine der Implementierungen ist standardkonform, da sie versuchen, sich std::hash<X>::operator()eher als std::hash<X>Ganzes zu spezialisieren , und die Signatur von std::hash<T>::operator()ist implementierungsdefiniert.
ildjarn

@ildjarn: Klargestellt - Ich habe über die Bibliotheksimplementierungen gesprochen, nicht über die versuchten Spezialisierungen. Ich bin mir nicht sicher, was genau das OP fragen wollte.
Aschepler
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.