Должен ли std::hash‹T› работать, когда T — это std::pair‹два более простых типа, также поддерживаемых std::hash›?

Я использовал упорядоченный набор, объявленный так:

std::set<std::pair<const std::string, const myClass *> > myset;

После некоторого анализа того, как я использовал set,, я пришел к выводу, что unordered_set был бы более разумным выбором. Но когда я изменил std::set на std::unordered_set, я получил огромное количество сообщений об ошибках от моего компилятора (g++ 4.8.1), жалующихся на

invalid use of incomplete type struct std::hash<std::pair<const std::basic_string<char>, const myClass * > >

Я понял, что std::hash не знает, как работать с типом, который был std::pair, несмотря на то, что каждый из двух типов, составляющих pair, был хэшируемым. Я думаю, что ошибка для хеш-функции пары целых чисел содержит соответствующие информация о стандарте C++11, которая объясняет, почему что-то пошло не так. (Нет хорошего объяснения непроницаемой стене текста ошибки, которую выдает для этого g++.)

Мне кажется, что

std::hash<std::pair<T1, T2>> hasher(make_pair(x,y))
  = some_func(std::hash<T1>hasher(x), std::hash<T2>hasher(y) )

где some_func() может быть таким же простым, как XOR (или нет; см. Почему является ли XOR способом объединения хэшей по умолчанию?)

Есть ли веская причина для того, чтобы стандарт не требовал std::hash знать, как построить хеш-значение для объекта, который представляет собой pair типов, каждый из которых может быть хеширован?


person jzions    schedule 14.01.2015    source источник
comment
Можешь хешировать const myClass *? Или вы можете только хешировать const myClass?   -  person Dietrich Epp    schedule 14.01.2015
comment
Существует предложение по решению этой проблемы. Кроме того, этот вопрос слишком широк и на него нельзя ответить. На данный момент вы можете указать свою собственную специализацию std::hash и использовать boost::hash_combine для объединения отдельных хэшей.   -  person Praetorian    schedule 14.01.2015
comment
Пожалуйста, не стесняйтесь использовать программное обеспечение, стоящее за предложением, на которое ссылается Praetorian: github.com/HowardHinnant/hash_append Просто за последние пару дней моя команда извлекла выгоду из возможности легко сравнить хеш-алгоритм X с хеш-алгоритмом Y применительно к нашему конкретному приложению и выбрать, какой из них лучше для нас.   -  person Howard Hinnant    schedule 15.01.2015
comment
XOR — хреновый способ объединения хэшей   -  person Yakk - Adam Nevraumont    schedule 30.04.2015


Ответы (1)


Причина проста, его не добавили в стандарт. То же самое относится и к хэшированию других структур, таких как tuple.

Вещи, как правило, добавляются к стандарту, когда они достаточно хороши, а не когда они совершенны, поскольку совершенство — враг хорошего. Дополнительные специализации std::hash не могут нарушить код (часто), поэтому добавление новых относительно безвредно.

В любом случае, для этого мы можем написать свои собственные расширители хэшей. Например:

namespace hashers {
  constexpr size_t hash_combine( size_t, size_t ); // steal from boost, or write your own
  constexpr size_t hash_combine( size_t a ) { return a; }
  constexpr size_t hash_combine() { return 0; }
  template<class...Sizes>
  constexpr size_t hash_combine( size_t a, size_t b, Sizes... sizes ) {
    return hash_combine( hash_combine(a,b), sizes... );
  }

  template<class T=void> struct hash;

  template<class A, class B>
  constexpr size_t custom_hash( std::pair<A,B> const& p ) {
    return hash_combine( hash<size_t>{}(2), hash<std::decay_t<A>>{}(p.first), hash<std::decay_t<B>>{}(p.second) );
  }
  template<class...Ts, size_t...Is>
  constexpr size_t custom_hash( std::index_sequence<Is...>, std::tuple<Ts...> const& p ) {
    return hash_combine( hash<size_t>{}(sizeof...(Ts)), hash<std::decay_t<Ts>>{}(std::get<Is>(p))... );
  }
  template<class...Ts>
  constexpr size_t custom_hash( std::tuple<Ts...> const& p ) {
    return custom_hash( std::index_sequence_for<Ts...>{}, p );
  }
  template<class T0, class C>
  constexpr size_t custom_hash_container( size_t n, C const& c) {
    size_t retval = hash<size_t>{}(n);
    for( auto&& x : c)
      retval = hash_combine( retval, hash<T>{}(x) );
    return retval;
  }
  template<class T0, class C>
  constexpr size_t custom_hash_container( C const& c) {
    return custom_hash_container( c.size(), c );
  }
  template<class T, class...Ts>
  size_t custom_hash( std::vector<T, Ts...> const& v ) {
    return custom_hash_container<T>(v);
  }
  template<class T, class...Ts>
  size_t custom_hash( std::basic_string<T, Ts...> const& v ) {
    return custom_hash_container<T>(v);
  }
  template<class T, size_t n>
  constexpr size_t custom_hash( std::array<T, n> const& v ) {
    return custom_hash_container<T>(n, v);
  }
  template<class T, size_t n>
  constexpr size_t custom_hash( T (const& v)[n] ) {
    return custom_hash_container<T>(n, v);
  }
  // etc -- list, deque, map, unordered map, whatever you want to support
  namespace details {
    template<class T, class=void>
    struct hash : std::hash<T> {};
    using hashers::custom_hash;
    template<class T>
    struct hash<T,decltype(void(
      custom_hash(declval<T const&>())
    )) {
      constexpr size_t operator()(T const& t)const {
        return custom_hash(t);
      }
    };
  }
  template<class T>
  struct hash : details::hash<T> {};
  template<>
  struct hash<void> {
    template<class T>
    constexpr size_t operator()(T const& t)const { return hash<T>{}(t); }
  }
}

и теперь hashers::hash<T> будет рекурсивно использовать либо функцию custom_hash с поиском в ADL, либо std::hash, если это не удается, для хэширования T и его компонентов, а hashers::hash<> — это универсальный хэшер, который пытается хешировать все, что ему передается.

Код может не компилироваться, как показано.

Я решил хешировать все контейнеры и кортежи как их длину, а затем хешировать комбинацию их содержимого. В качестве побочного эффекта array<int, 3> хеширует так же, как tuple<int,int,int>, tuple<int,int> хэширует так же, как pair<int,int>, а std::vector<char>{'a','b','c', '\0'} хэширует так же, как "abc", что я считаю хорошим свойством. Пустой массив/кортеж/вектор/и т. д. хэширует как size_t(0).

Вы можете расширить описанную выше систему для своих собственных типов, просто переопределив custom_hash в пространстве имен рассматриваемого типа или настроив либо std::hash<X>, либо hashers::hash<X> для создания собственного хэша (я бы выбрал std::hash по принципу наименьшего удивления самого себя). Для расширенного использования вы можете специализировать hashers::details::hash<X,void> с помощью SFINAE, но я бы посоветовал сделать это вместо custom_hash.

person Yakk - Adam Nevraumont    schedule 14.01.2015
comment
Улучшенная структура хэширования, нет необходимости в комбинировании хэшей: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html - person Maxim Egorushkin; 05.10.2020
comment
Предостережение: при написании собственного хэша для неупорядоченных контейнеров следует учитывать их нетривиальные == - person Caleth; 05.10.2020
comment
@Caleth Я не уверен, к чему относится это предостережение. Если два неупорядоченных контейнера возвращают == true, ваш хэш должен быть одинаковым, но я не вижу никаких советов или реализаций выше, которые не согласны с этим. Вы можете это указать? - person Yakk - Adam Nevraumont; 05.10.2020
comment
Наивный hash_combine_range(unordered.begin(), unordered.end()) не обязательно сработает - person Caleth; 05.10.2020
comment
@caleth ах, потому что два неупорядоченных контейнера с разным количеством сегментов повторяются по-разному. - person Yakk - Adam Nevraumont; 05.10.2020