Вопрос
Что такое хорошая специализация std :: hash для использования в третьем параметре шаблона std :: unordered_map или std :: unordered_set для определенного пользователем типа, для которого все типы данных-членов уже имеют хорошую специализацию std :: hash?
Для этого вопроса я определяю «хороший» как простой для реализации и понимания, достаточно эффективный и маловероятный, чтобы вызвать коллизии хеш-таблиц. Определение товара не включает никаких заявлений о безопасности.
Состояние того, что Google’able
На данный момент два вопроса StackOverflow являются первыми хитами в поиске Google «специализация std hash».
Первый, Как специализировать std :: hash :: operator () для пользовательского типа в неупорядоченных контейнерах?, адреса, разрешено ли открывать пространство имен std и добавлять специализации шаблона.
Второй, Как специализировать std :: hash для типа из другой библиотеки, по сути, решает тот же вопрос.
Это оставляет текущий вопрос. Учитывая, что реализации Стандартной библиотеки C ++ определили хеш-функции для примитивных типов и типов в Стандартной библиотеке, каков простой и эффективный способ специализации std :: hash для пользовательских типов? Есть ли хороший способ объединить хэш-функции, предоставляемые реализацией стандартной библиотеки?
(Редактировать спасибо Dyp.) Другой вопрос на StackOverflow адресов, как объединить пара хеш-функций.
Другие результаты Google больше не помогают.
это В статье доктора Доббса говорится, что XOR из двух удовлетворительных хешей даст новый удовлетворительный хеш.
это статьи, кажется, говорят от знания и подразумевают много вещей, но легки на деталях. Это противоречит статье доктора Доббса в кратком замечании в первом примере о том, что использование XOR для объединения хеш-функций приводит к слабой результирующей хеш-функции.
Поскольку XOR, примененный к любым двум равным значениям, приводит к 0, я могу понять, почему XOR сам по себе слабый.
Мета Вопрос
Хорошо приветствуемый ответ, объясняющий, почему этот вопрос недействителен и на него нельзя ответить вообще, также приветствуется.
Одним из простых способов является использование boost::hash
библиотека и расширить его для вашего типа. Имеет хорошую функцию расширения hash_combine
(std::hash
не хватает этого), что позволяет легко составлять хэши отдельных данных членов ваших структур.
Другими словами:
boost::hash_value
для вашего собственного типа.std::hash
для вашего собственного типа и реализовать его с помощью boost::hash_value
,Таким образом, вы получите лучшее из стандартного и усиленного миров std::hash<>
а также boost::hash<>
работа для вашего типа.
Лучше использовать предложенную новую инфраструктуру хеширования в N3980 Типы Не знаю #. Эта инфраструктура делает hash_combine
ненужным.
Во-первых, статья доктора Доббса, в которой говорится, что XOR двух
удовлетворительные хеши будут производить удовлетворительные хэши просто
неправильно. Это хороший рецепт для плохих хэшей. В общем, чтобы
создать хороший хеш, вы начинаете с разложения вашего объекта на
подобъекты, каждый из которых существует хороший хэш, и
объединяя хэши. Один простой способ сделать это что-то
лайк:
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
прямо и явно
укажите, что если вам нужен хеш такого контейнера.
Пока мы не получим библиотеку в стандарте, чтобы помочь с этим:
std::hash<YourType>
, создать SpookyHash
экземпляр и Init
Это. Обратите внимание, что выбор случайного числа при запуске процесса или std::hash
строительство, и использование этого в качестве инициализации будет сделать DoS вашу программу немного сложнее, но не решает проблему.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
пример.SpookyHash::Final
от std::hash<YourType>
,Ваша операция необходимо в
size_t
==
оператор.Нет явного требования, чтобы хеш-значения были равномерно распределены по диапазону size_t
целые числа. cppreference.com
заметки тот
некоторые реализации [стандартной библиотеки] используют тривиальные (идентифицирующие) хеш-функции, которые отображают целое число на себя
Предотвращение коллизий хешей в сочетании с этой слабостью означает, что специализация 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
, Поэтому хэши Point
также будет в диапазоне [0,255], с 256 (= 2 ^ 8) возможных значений. Есть 256 * 256 (= 2 ^ 16) уникальных Point
объекты (а 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 >
(в этом случае) пытается распространить значения хеш-кода по std::size_t
спектр.
Накопление значений хеш-кода компонента с использованием метода умножения на простое и сложное, как обычно делается на Java, наверное лучше работает вообще
namespace std {
template<>
struct hash< Point > {
size_t operator()(const Point &p) const {
const size_t prime = 257;
size_t h {hash< uint8_t >(p.x)};
h = h * prime + hash< uint8_t >(p.y);
return h;
}
};
}