Come specializzare std :: hash <Key> :: operator () per il tipo definito dall'utente in contenitori non ordinati?


101

Per supportare i tipi di chiave definiti dall'utente in std::unordered_set<Key>e std::unordered_map<Key, Value> si deve fornire operator==(Key, Key)un hash funtore:

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;

Sarebbe più conveniente scrivere solo std::unordered_set<X> con un hash predefinito per il tipo X, come per i tipi forniti con il compilatore e la libreria. Dopo aver consultato

sembra possibile specializzarsi 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 
}                                                                             

Dato che il supporto del compilatore per C ++ 11 è ancora sperimentale --- non ho provato Clang ---, queste sono le mie domande:

  1. È legale aggiungere una tale specializzazione allo spazio dei nomi std? Ho sentimenti contrastanti al riguardo.

  2. Quale delle std::hash<X>::operator()versioni, se esiste, è conforme allo standard C ++ 11?

  3. C'è un modo portatile per farlo?


Con gcc 4.7.2, ho dovuto fornire un globaleoperator==(const Key, const Key)
Victor Lyuboslavsky

Nota che la specializzazione di std::hash(a differenza di altre cose nello stdspazio dei nomi) è scoraggiata dalla guida di stile di Google ; prendilo con un granello di sale.
Franklin Yu

Risposte:


128

Sei espressamente autorizzato e incoraggiato ad aggiungere specializzazioni allo spazio dei nomi std*. Il modo corretto (e fondamentalmente unico) per aggiungere una funzione hash è questo:

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

(Altre specializzazioni popolari che potresti considerare di supportare sono std::less, std::equal_toe std::swap.)

*) fintanto che uno dei tipi coinvolti è definito dall'utente, suppongo.


3
sebbene sia possibile, in generale consiglieresti di farlo in questo modo? Preferirei unorder_map<eltype, hash, equality>invece l' istanza , per evitare di rovinare la giornata di qualcuno con divertenti attività ADL. ( Modifica il consiglio di Pete Becker su questo argomento )
guarda il

2
@sehe: Se hai un funtore hash in giro che è costruibile per impostazione predefinita, forse, ma perché? (L'uguaglianza è più facile, dato che implementeresti semplicemente member- operator==.) La mia filosofia generale è che se la funzione è naturale ed essenzialmente l'unica "corretta" (come il confronto lessicografico delle coppie), la aggiungo a std. Se è qualcosa di peculiare (come il confronto di coppie non ordinato), lo rendo specifico per un tipo di contenitore.
Kerrek SB

3
Non sono in disaccordo, ma dove nello standard siamo autorizzati e incoraggiati ad aggiungere specializzazioni a std?
razeh

3
@Kerrek, sono d'accordo, ma speravo in un capitolo e un riferimento in versi a un posto nello standard. Ho trovato la dicitura che consente l'iniezione in 17.6.4.2.1 dove si dice che non è consentita "se non diversamente specificato", ma non sono stato in grado di trovare la parte "altrimenti specificata" nella specifica della pagina 4000+.
razeh

3
@razeh qui puoi leggere "Un programma può aggiungere una specializzazione modello per qualsiasi modello di libreria standard allo spazio dei nomi std solo se la dichiarazione dipende da un tipo definito dall'utente e la specializzazione soddisfa i requisiti della libreria standard per il modello originale e non è esplicitamente proibita . ". Quindi questa soluzione è ok.
Marek R

7

La mia scommessa sarebbe sull'argomento del modello Hash per le classi 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
}

Ovviamente

  • hashX potrebbe anche essere una funzione statica globale
  • nel secondo caso, potresti passarlo
    • l'oggetto funtore vecchio stile ( struct Xhasher { size_t operator(const X&) const; };)
    • std::hash<X>()
    • qualsiasi espressione vincolante che soddisfi la firma -

Apprezzo che tu possa scrivere qualcosa che non ha un costruttore predefinito, ma trovo sempre che richiedere a ogni costruzione di mappa di ricordare l'argomento aggiuntivo sia un po 'un fardello, un po' troppo per i miei gusti. Sono d'accordo con un esplicito argomento del modello, anche se la specializzazione std::hashè ancora la via più simpatica :-)
Kerrek SB

tipi definiti dall'utente in soccorso :-) Più seriamente, spero che li daremmo uno schiaffo sui polsi già nella fase in cui la loro classe contiene un char*!
Kerrek SB

Hmm ... hai un esempio di come una hashspecializzazione interferisce tramite ADL? Voglio dire, è del tutto plausibile, ma ho difficoltà a trovare un caso problematico.
Kerrek SB


È tutto divertente e giochi fino a quando non hai bisogno di un std::unordered_map<Whatever, Xunset>e non funziona perché il tuo Xunsettipo di hasher non è costruibile di default.
Brian Gordon

4

@Kerrek SB ha coperto 1) e 3).

2) Anche se g ++ e VC10 si dichiarano std::hash<T>::operator()con firme diverse, entrambe le implementazioni della libreria sono conformi allo Standard.

Lo Standard non specifica i membri di std::hash<T>. Dice solo che ciascuna di queste specializzazioni deve soddisfare gli stessi requisiti "hash" necessari per il secondo argomento del modello std::unordered_sete così via. Vale a dire:

  • Il tipo hash Hè un oggetto funzione, con almeno un tipo di argomento Key.
  • H è una copia costruibile.
  • H è distruttibile.
  • Se hè un'espressione di tipo Ho const H, ed kè un'espressione di un tipo convertibile in (possibilmente const) Key, allora h(k)è un'espressione valida con tipo size_t.
  • Se hè un'espressione di tipo Ho const H, ed uè un valore di tipo Key, allora h(u)è un'espressione valida con tipo size_tche non modifica u.

No, nessuna delle due implementazioni è conforme agli standard, poiché tentano di specializzarsi std::hash<X>::operator()piuttosto che std::hash<X>nel loro insieme e la firma di std::hash<T>::operator()è definita dall'implementazione.
ildjarn

@ildjarn: Chiarito: stavo parlando delle implementazioni della libreria, non delle specializzazioni tentate. Non sono sicuro di quale esattamente l'OP intendesse chiedere.
aschepler
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.