Como especializar std :: hash <Key> :: operator () para o tipo definido pelo usuário em contêineres não ordenados?

101

Para oferecer suporte a tipos de chaves definidas pelo usuário em std::unordered_set<Key>e std::unordered_map<Key, Value> é necessário fornecer operator==(Key, Key)um functor hash:

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;

Seria mais conveniente escrever apenas std::unordered_set<X> com um hash padrão para o tipo X, como para os tipos que vêm junto com o compilador e a biblioteca. Depois de consultar

parece possível se especializar 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 
}                                                                             

Dado que o suporte do compilador para C ++ 11 ainda é experimental --- eu não tentei o Clang ---, estas são as minhas perguntas:

  1. É legal adicionar essa especialização ao namespace std? Tenho sentimentos mistos sobre isso.

  2. Qual das std::hash<X>::operator()versões, se houver, é compatível com o padrão C ++ 11?

  3. Existe uma maneira portátil de fazer isso?

René Richter
fonte
Com o gcc 4.7.2, tive que fornecer um globaloperator==(const Key, const Key)
Victor Lyuboslavsky
Observe que a especialização de std::hash(ao contrário de outras coisas no stdnamespace) é desencorajada pelo guia de estilo do Google ; tome-o com um grão de sal.
Franklin Yu

Respostas:

128

Você está expressamente autorizado e encorajado a adicionar especializações ao namespace std*. A maneira correta (e basicamente única) de adicionar uma função hash é esta:

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

(Outras especializações populares que você pode considerar apoiar são std::less, std::equal_toe std::swap.)

*) contanto que um dos tipos envolvidos seja definido pelo usuário, suponho.

Kerrek SB
fonte
3
embora isso seja possível, em geral você recomendaria fazer dessa maneira? Eu prefiro instanciação em unorder_map<eltype, hash, equality>vez disso, para evitar estragar o dia de alguém com negócios ADL engraçados. ( Edite o conselho de Pete Becker sobre este tópico )
ver
2
@sehe: Se você tiver um functor hash por aí que seja construtível por padrão, talvez, mas por quê? (Igualdade é mais fácil, já que você apenas implementaria membro- operator==.) Minha filosofia geral é que se a função é natural e essencialmente a única "correta" (como comparação de pares lexicográficos), então eu a adiciono std. Se for algo peculiar (como comparação de pares não ordenados), eu o tornarei específico para um tipo de contêiner.
Kerrek SB
3
Não estou discordando, mas onde no padrão podemos e encorajamos adicionar especializações ao padrão?
razeh 02 de
3
@Kerrek, eu concordo, mas estava esperando por uma referência de capítulo e versículo a um lugar no padrão. Encontrei o texto que permite a injeção em 17.6.4.2.1, onde diz que não é permitido "a menos que especificado de outra forma", mas não consegui encontrar a parte "especificada de outra forma" na especificação de mais de 4000 páginas.
razeh 02 de
3
@razeh aqui você pode ler "Um programa pode adicionar uma especialização de modelo para qualquer modelo de biblioteca padrão ao namespace std apenas se a declaração depender de um tipo definido pelo usuário e a especialização atender aos requisitos de biblioteca padrão para o modelo original e não for explicitamente proibida . ". Portanto, esta solução está ok.
Marek R
7

Minha aposta seria no argumento do modelo Hash para as 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
}

Claro

  • hashX poderia muito bem ser uma função estática global
  • no segundo caso, você poderia passar que
    • o antiquado objeto functor ( struct Xhasher { size_t operator(const X&) const; };)
    • std::hash<X>()
    • qualquer expressão de ligação que satisfaça a assinatura -
ver
fonte
Eu aprecio que você possa escrever algo que não tenha um construtor padrão, mas eu sempre acho que exigir que cada construção de mapa lembre o argumento adicional é um fardo - um fardo demais para o meu gosto. Estou bem com um argumento de modelo explícito, embora a especialização std::hashainda seja a melhor saída :-)
Kerrek SB
tipos definidos pelo usuário para o resgate :-) Mais a sério, espero que possamos dar um tapa em seus pulsos já no estágio em que sua classe contém um char*!
Kerrek SB
Hmm ... você tem um exemplo de como uma hashespecialização interfere via ADL? Quer dizer, é totalmente plausível, mas é difícil encontrar uma pasta de problemas.
Kerrek SB
É tudo diversão e jogos até você precisar de um std::unordered_map<Whatever, Xunset>e não funciona porque seu Xunsettipo de hasher não pode ser construído por padrão.
Brian Gordon,
4

@Kerrek SB cobriu 1) e 3).

2) Embora g ++ e VC10 declarem std::hash<T>::operator()com assinaturas diferentes, ambas as implementações de biblioteca são compatíveis com o padrão.

O padrão não especifica os membros de std::hash<T>. Diz apenas que cada especialização deve satisfazer os mesmos requisitos de "Hash" necessários para o segundo argumento de modelo de std::unordered_sete assim por diante. Nomeadamente:

  • O tipo hash Hé um objeto de função, com pelo menos um tipo de argumento Key.
  • H é uma cópia construtível.
  • H é destrutível.
  • Se hfor uma expressão do tipo Hou const H, e kfor uma expressão de um tipo conversível em (possivelmente const) Key, então h(k)é uma expressão válida com tipo size_t.
  • Se hfor uma expressão do tipo Hou const H, e ufor um valor l do tipo Key, então h(u)é uma expressão válida com o tipo size_tque não se modifica u.
Aschepler
fonte
Não, nenhuma implementação é compatível com o padrão, uma vez que eles tentam se especializar em std::hash<X>::operator()vez de std::hash<X>como um todo, e a assinatura de std::hash<T>::operator()é definida pela implementação.
ildjarn de
@ildjarn: Esclarecido - eu estava falando sobre as implementações da biblioteca, não as tentativas de especialização. Não tenho certeza do que exatamente o OP pretendia perguntar.
aschepler de