Является ли специализация с плавающей запятой std::hash
(скажем, для double
с или float
s) надежный относительно почти равенство? То есть, если два значения (такие как (1./std::sqrt(5.)/std::sqrt(5.))
а также .2
) следует сравнить, но не буду делать это с ==
оператор, как будет std::hash
вести себя?
Итак, могу ли я положиться на double
как std::unordered_map
ключ к работе как положено?
Я видел «Хеширование значений с плавающей точкой«но это спрашивает о повышении; я спрашиваю о гарантиях C ++ 11.
std::hash
имеет одинаковые гарантии для всех типов, над которыми он может
быть созданным: если два объекта равны, их хеш-коды
быть равным В противном случае есть очень большая вероятность того, что они
не будет. Таким образом, вы можете положиться на double
в качестве ключа в
unordered_map
работать как положено: если двух двойников нет
равно (как определено ==
), они, вероятно, будут иметь разные
хэш (и даже если они этого не делают, они разные ключи, потому что
unordered_map
также проверяет на равенство).
Очевидно, что если ваши значения являются результатом неточных
расчеты, они не являются подходящими ключами для unordered_map
(ни, может быть, для любой карты).
Несколько проблем с этим вопросом:
Причина, по которой ваши два выражения не сравниваются как равные, заключается в том, что НЕ существует двух двоичных выражений по 0.2, но нет НИКАКОГО точного (конечного) двоичного представления 0.2
, или же sqrt(5)
! Так на самом деле, пока (1./std::sqrt(5.)/std::sqrt(5.))
а также .2
должны быть одинаковыми алгебраически, они могут не совпадать в компьютерной арифметике. (Они даже не в арифметике с ручкой на бумаге с конечной точностью. Допустим, вы работаете с 10 цифрами после десятичной точки. Выпишите sqrt(5)
с 10 цифрами и вычислить ваше первое выражение. Это не будет .2
.)
Конечно, у вас есть разумное представление о близких двух числах. На самом деле у вас есть как минимум два: один абсолютный (|a-b| < eps
), один родственник. Но это не приводит к разумным хэшам. Если вы хотите, чтобы все числа в eps
друг друга, чтобы иметь тот же хэш, то 1, 1+eps, 1+2*eps, ...
все они будут иметь одинаковый хэш и, следовательно, все числа будут иметь одинаковый хэш. Это допустимая, но бесполезная хеш-функция. Но это единственный, который удовлетворяет вашему требованию сопоставления близких значений в один и тот же хэш!
Не существует строгого понятия «почти равенство». Таким образом, поведение не может быть гарантировано в принципе. Если вы хотите определить свою собственную концепцию «почти равных» и построить хеш-функцию таким образом, чтобы два «почти равных» числа с плавающей точкой имели одинаковый хэш, вы можете это сделать. Но тогда это будет справедливо только для вашего конкретного понятия «почти равных» поплавков.
За хешированием по умолчанию unordered_map
Eсть std::hash
структура, которая обеспечивает operator()
вычислить хэш заданного значения.
Доступен набор специализаций по умолчанию для этих шаблонов, включая std::hash<float>
а также std::hash<double>
,
На моей машине (LLVM + clang) они определены как
template <>
struct hash<float> : public __scalar_hash<float>
{
size_t operator()(float __v) const _NOEXCEPT
{
// -0.0 and 0.0 should return same hash
if (__v == 0)
return 0;
return __scalar_hash<float>::operator()(__v);
}
};
где __scalar_hash
определяется как:
template <class _Tp>
struct __scalar_hash<_Tp, 0> : public unary_function<_Tp, size_t>
{
size_t operator()(_Tp __v) const _NOEXCEPT
{
union
{
_Tp __t;
size_t __a;
} __u;
__u.__a = 0;
__u.__t = __v;
return __u.__a;
}
};
Где в основном хеш создается путем установки значения объединения в исходное значение, а затем получения просто части, которая является большой как size_t
,
Таким образом, вы получаете некоторые дополнения или усеченное значение, но это на самом деле не имеет значения, потому что, как вы можете видеть, необработанные биты числа используются для вычисления хэша, это означает, что оно работает точно так же, как ==
оператор. Два плавающих числа, чтобы иметь одинаковый хеш (исключая коллизию, заданную усечением), должны иметь одинаковое значение.