Comment spécialiser std :: hash <Key> :: operator () pour le type défini par l'utilisateur dans des conteneurs non ordonnés?

101

Pour prendre en charge les types de clés définis par l'utilisateur dans std::unordered_set<Key>et std::unordered_map<Key, Value> il faut fournir operator==(Key, Key)un foncteur de hachage:

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;

Il serait plus pratique d'écrire uniquement std::unordered_set<X> avec un hachage par défaut pour le type X, comme pour les types accompagnant le compilateur et la bibliothèque. Après consultation

  • Projet de norme C ++ N3242 §20.8.12 [unord.hash] et §17.6.3.4 [hash.requirements],
  • Boost.
  • g ++ include\c++\4.7.0\bits\functional_hash.h
  • VC10 include\xfunctional
  • diverses questions connexes dans Stack Overflow

il semble possible de se spécialiser 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 
}                                                                             

Étant donné que le support du compilateur pour C ++ 11 est encore expérimental --- je n'ai pas essayé Clang ---, voici mes questions:

  1. Est-il légal d'ajouter une telle spécialisation à l'espace de noms std ? J'ai des sentiments mitigés sur ce sujet.

  2. Laquelle des std::hash<X>::operator()versions, le cas échéant, est conforme à la norme C ++ 11?

  3. Existe-t-il un moyen portable de le faire?

René Richter
la source
Avec gcc 4.7.2, je devais fournir un globaloperator==(const Key, const Key)
Victor Lyuboslavsky
Notez que la spécialisation de std::hash(contrairement à d'autres choses dans l' stdespace de noms) est déconseillée par le guide de style de Google ; Prenez-le avec un grain de sel.
Franklin Yu

Réponses:

128

Vous êtes expressément autorisé et encouragé à ajouter des spécialisations à l'espace de noms std*. La manière correcte (et essentiellement la seule) d'ajouter une fonction de hachage est la suivante:

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

(D'autres spécialisations populaires que vous pourriez envisager de prendre en charge sont std::less, std::equal_toet std::swap.)

*) tant que l'un des types impliqués est défini par l'utilisateur, je suppose.

Kerrek SB
la source
3
bien que cela soit possible, recommanderiez-vous en général de le faire de cette façon? Je préférerais unorder_map<eltype, hash, equality>plutôt l' instanciation , pour éviter de gâcher la journée de quelqu'un avec des affaires ADL amusantes. ( Modifier conseils de Pete Becker sur ce sujet )
sehe
2
@sehe: Si vous avez un foncteur de hachage qui traîne, c'est peut-être constructible par défaut, mais pourquoi? (L'égalité est plus facile, puisque vous implémentez simplement member- operator==.) Ma philosophie générale est que si la fonction est naturelle et essentiellement la seule "correcte" (comme la comparaison de paires lexicographiques), alors je l'ajoute std. Si c'est quelque chose de particulier (comme la comparaison de paires non ordonnées), je le rends spécifique à un type de conteneur.
Kerrek SB
3
Je ne suis pas en désaccord, mais où dans la norme sommes-nous autorisés et encouragés à ajouter des spécialisations à std?
razeh le
3
@Kerrek, je suis d'accord, mais j'espérais une référence de chapitre et de verset à une place dans la norme. J'ai trouvé le libellé permettant l'injection à 17.6.4.2.1 où il est dit qu'il n'est pas autorisé "sauf indication contraire", mais je n'ai pas été en mesure de trouver la partie "autrement spécifiée" parmi la spécification de plus de 4000 pages.
razeh le
3
@razeh ici, vous pouvez lire "Un programme peut ajouter une spécialisation de modèle pour tout modèle de bibliothèque standard à l'espace de noms std uniquement si la déclaration dépend d'un type défini par l'utilisateur et que la spécialisation répond aux exigences de la bibliothèque standard pour le modèle d'origine et n'est pas explicitement interdite . ". Donc, cette solution est correcte.
Marek R
7

Mon pari serait sur l'argument du modèle Hash pour les classes 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
}

Bien sûr

  • hashX pourrait tout aussi bien être une fonction statique globale
  • dans le second cas, tu pourrais passer ça
    • l'objet foncteur démodé ( struct Xhasher { size_t operator(const X&) const; };)
    • std::hash<X>()
    • toute expression de liaison satisfaisant la signature -
sehe
la source
J'apprécie que vous puissiez écrire quelque chose qui n'a pas de constructeur par défaut, mais je trouve toujours que demander à chaque construction de carte de se souvenir de l'argument supplémentaire est un peu un fardeau - un peu trop lourd à mon goût. Je suis d'accord avec un argument de modèle explicite, bien que la spécialisation std::hashsoit toujours la meilleure solution :-)
Kerrek SB
les types définis par l'utilisateur à la rescousse :-) Plus sérieusement, j'espère que nous les giflerons déjà au stade où leur classe contient un char*!
Kerrek SB
Hmm ... avez-vous un exemple de la façon dont une hashspécialisation interfère via ADL? Je veux dire, c'est tout à fait plausible, mais j'ai du mal à trouver un cas problématique.
Kerrek SB
Tout est amusant et amusant jusqu'à ce que vous en ayez besoin std::unordered_map<Whatever, Xunset>et cela ne fonctionne pas car votre Xunsettype de hachage n'est pas constructible par défaut.
Brian Gordon
4

@Kerrek SB a couvert 1) et 3).

2) Même si g ++ et VC10 déclarent std::hash<T>::operator()avec des signatures différentes, les deux implémentations de bibliothèque sont conformes au Standard.

La norme ne spécifie pas les membres de std::hash<T>. Il dit simplement que chacune de ces spécialisations doit satisfaire les mêmes exigences de "Hash" que celles requises pour le deuxième argument de modèle de std::unordered_setet ainsi de suite. À savoir:

  • Le type de hachage Hest un objet fonction, avec au moins un type d'argument Key.
  • H est une copie constructible.
  • H est destructible.
  • Si hest une expression de type Hou const H, et kest une expression d'un type convertible en (éventuellement const) Key, alors h(k)est une expression valide avec type size_t.
  • Si hest une expression de type Hou const H, et uest une lvaleur de type Key, alors h(u)est une expression valide avec un type size_tqui ne modifie pas u.
aschepler
la source
Non, aucune des implémentations n'est conforme à la norme, car elles tentent de se spécialiser std::hash<X>::operator()plutôt que std::hash<X>dans son ensemble, et la signature de std::hash<T>::operator()est définie par l'implémentation.
ildjarn
@ildjarn: Clarifié - je parlais des implémentations de bibliothèques, pas des tentatives de spécialisation. Je ne sais pas exactement quel OP voulait demander.
aschepler