Как специализировать std:: hash для пользовательских типов?

Вопрос

какова хорошая специализация std::hash для использования в третьем параметре шаблона std::unordered_map или std::unordered_set для определенного пользователем типа, для которого все типы данных-членов уже имеют хорошую специализацию std:: hash?

для этого вопроса я определяю "хороший" как простой в реализации и понимании, разумно эффективный и маловероятный для создания коллизий хэш-таблиц. Определением добра не включает заявления о безопасности.

состояние того, что Google'able

на данный момент два вопроса StackOverflow являются первыми хитами для поиска Google "специализации хэша std".

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

второе, как специализируйте std:: hash для типа из другой библиотеки, по существу, решает тот же вопрос.

Это оставляет текущий вопрос. Учитывая, что реализации стандартной библиотеки C++ определяют хэш-функции для примитивных типов и типов в стандартной библиотеке, каков простой и эффективный способ специализации std::hash для пользовательских типов? Есть ли хороший способ объединить хэш-функции, предоставляемые стандартной реализацией библиотеки?

(редактировать спасибо дип.) еще вопрос на адресах StackOverflow, как объединить пара хэш-функции.

другие результаты Google больше не помогают.

этой в статье доктора Доббса говорится, что XOR двух удовлетворительных хэшей произведет новый удовлетворительный хэш.

этой статьи, кажется, говорят от знания и подразумевает много вещей, но свет на деталях. Это противоречит статье доктора Доббса в краткое замечание в первом примере, говоря, что использование XOR для объединения хэш-функций делает для слабой результирующей хэш-функции.

поскольку XOR, примененный к любым двум равным значениям, приводит к 0, я могу понять, почему XOR сам по себе слаб.

Мета Вопрос

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

4 ответов


один простой способ-использовать boost::hash библиотеки и продлить его для вашего типа. Он имеет хорошую функцию расширения hash_combine (std::hash не хватает этого), что позволяет легко составлять хэши отдельных элементов данных ваших структур.

другими словами:

  1. перегрузка boost::hash_value для своего типа.
  2. Specialize std::hash для вашего собственного типа и реализовать его с помощью boost::hash_value.

таким образом, вы получите лучшее из std и boost миров, как std::hash<> и boost::hash<> работа для вашего типа.


лучший способ-использовать предлагаемую новую инфраструктуру хэширования в типы N3980 не знают #. Эта инфраструктура делает hash_combine ненужных.


во-первых, статья доктора Доббса, в которой говорится, что XOR из двух удовлетворительные хэши будут производить удовлетворительный хэш просто неправильный. Это хороший рецепт для бедных хэшей. В общем, создайте хороший хэш, вы начинаете с разложения вашего объекта на подобъекты, каждый из которых имеет хороший хэш, и сочетая hashs. Один простой способ сделать это что-то например:

class HashAccumulator
{
    size_t myValue;
public:
    HashAccumulator() : myValue( 2166136261U ) {}
    template <typename T>
    HashAccumulator& operator+=( T const& nextValue )
    {
        myValue = 127U * myValue + std::hash<T>( nextHashValue );
    }
    HashAccumulator operator+( T const& nextHashValue ) const
    {
        HashAccumulator results( *this );
        results += nextHashValue;
        return results;
    }
};

(это было разработано так, что вы можете использовать std::accumulate если у вас есть последовательность ценности.)

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


пока мы не получим библиотеку в стандарте, чтобы помочь с этим:

  1. скачать современный hasher, как SpookyHash:http://burtleburtle.net/bob/hash/spooky.html.
  2. в определении std::hash<YourType> создать SpookyHash экземпляра,Init его. Обратите внимание, что выбор случайного числа при запуске процесса или std::hash строительство, и использование этого в качестве инициализации будет сделать его немного сложнее DoS вашей программы, но не исправить проблема.
  3. возьмите каждое поле в вашей структуре, которое способствует operator== ("видимое поле"), и кормить его в SpookyHash::Update.
    • остерегайтесь таких типов, как double: у них есть 2 представления char[] для сравнения ==: -0.0 и 0.0. Также остерегайтесь типов, которые имеют прокладку. На большинстве машин, int не делает, но трудно сказать, если struct будет. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html#is_contiguously_hashable обсуждает это.
    • если у вас есть подструктуры, вы получите более быстрое и качественное хэш-значение от рекурсивного ввода своих полей в одно и то же SpookyHash экземпляра. Однако для этого требуется добавить метод к этим структурам или вручную извлечь заметные поля: если вы не можете этого сделать, допустимо просто подать их std::hash<> значение в верхний уровень SpookyHash пример.
  4. обратный вывод SpookyHash::Final С std::hash<YourType>.

операции требуется to

  • возвращает значение типа size_t
  • соответствует == оператора.
  • имеют низкую вероятность хэш-столкновения для неравных значений.

нет явного требования, что хэш-значения равномерно распределены по диапазону size_t - целые числа. cppreference.com Примечания это

некоторые реализации [of стандартная библиотека] используйте тривиальные (идентификационные) хэш-функции, которые сопоставляют целое число с самим собой

избежание хэш-коллизий в сочетании с этой слабостью означает, что специализация std::hash для ваших типов должен никогда просто используйте (быстрый) побитовый XOR (^), чтобы объединить суб-хэши ваших данных-членов. Рассмотрим следующий пример:

 struct Point {
    uint8_t x;
    uint8_t y;
 };

 namespace std {
    template<>
    struct hash< Point > {
       size_t operator()(const Point &p) const {
          return hash< uint8_t >(p.x) ^ hash< uint8_t >(p.y);
       }
    };
 }

хэши p.x будет в диапазоне [0,255], а также хэши p.y. Поэтому хэши a Point также будет в диапазоне [0,255] с 256 (=2^8) возможными значениями. Есть 256*256 (=2^16) уникальный Point объекты (a std::size_t обычно поддерживает значения 2^32 или 2^64). Поэтому вероятность хэш-столкновения на хороший функция хэширования должна быть приблизительно 2^(-16). Наша функция дает вероятность хэш-столкновения чуть менее 2^(-8). Это ужасно: наш хэш предоставляет только 8 бит информации, но хороший хэш должен обеспечить 16 бит информации.

если функции хэширования ваших членов данных предоставляют только хэш-значения в нижних частях std::size_t диапазон, вы должны "сдвинуть" биты хэша компонента перед их объединением, чтобы каждый из них вносил независимые биты информации. Выполнение левой смены выглядит просто

       return (hash< uint8_t >(p.x) << 8) ^ hash< uint8_t >(p.y);

но это удалить информация (из-за переполнения) при осуществлении hash< uint8_t > (в данном случае) попытки распространения значения хэш-кода над