Таким образом, есть несколько проблем с использованием парных символов в качестве ключей в std::map
.
Во-первых, NaN
, который сравнивает меньше, чем он сам, является проблемой.Если есть вероятность вставки NaN
, используйте это:
struct safe_double_less {
bool operator()(double left, double right) const {
bool leftNaN = std::isnan(left);
bool rightNaN = std::isnan(right);
if (leftNaN != rightNaN)
return leftNaN<rightNaN;
return left<right;
}
};
, но это может быть чрезмерно параноидальным.Не повторяю, не включайте порог epsilon в свой оператор сравнения, который вы передаете std::set
или тому подобное: это нарушит требования заказа контейнера и приведет к непредсказуемому неопределенному поведению.
(Я поставил NaN
больше, чем все double
с, включая +inf
, в моем порядке, без уважительной причины. Менее чем все double
с тоже подойдут).
Так что используйте либопо умолчанию operator<
, или выше safe_double_less
, или что-то подобное.
Далее, я бы посоветовал использовать std::multimap
или std::multiset
, потому что вы должны ожидать нескольких значений для каждого поиска.С тем же успехом вы можете сделать управление контентом повседневным делом, а не угловым делом, чтобы расширить охват тестирования вашего кода.(Я бы редко рекомендовал эти контейнеры) Плюс к этому блоки operator[]
, которые не рекомендуется использовать при использовании ключей с плавающей запятой.
Точка, в которой вы хотите использовать эпсилон, - это когда вы запрашиваетеконтейнер.Вместо использования прямого интерфейса создайте вспомогательную функцию, подобную этой:
// works on both `const` and non-`const` associative containers:
template<class Container>
auto my_equal_range( Container&& container, double target, double epsilon = 0.00001 )
-> decltype( container.equal_range(target) )
{
auto lower = container.lower_bound( target-epsilon );
auto upper = container.upper_bound( target+epsilon );
return std::make_pair(lower, upper);
}
, которая работает как на std::map
, так и на std::set
(и multi
версиях).
(Inболее современная кодовая база, я ожидаю, что объект range<?>
будет лучше возвращать из функции equal_range
. Но сейчас я сделаю его совместимым с equal_range
).
Это находит диапазон вещей, чьи ключи "достаточно близки" к тому, который вы запрашиваете, в то время как контейнер поддерживает свои гарантии упорядочения внутри и не выполняет неопределенное поведение.
Для проверки существованияключ, сделайте это:
template<typename Container>
bool key_exists( Container const& container, double target, double epsilon = 0.00001 ) {
auto range = my_equal_range(container, target, epsilon);
return range.first != range.second;
}
и если вы хотите удалить / заменить записи, вам следует иметь дело с возможностью того, что может быть более одного попадания в запись.
Короче ответ«не используйте значения с плавающей запятой в качестве ключей для std::set
и std::map
», потому что это немного хлопотно.
Если вы используете ключи с плавающей запятой для std::set
или std::map
почти наверняка никогда делать .find
или []
на них, так как это весьма вероятно, будет источником ошибок.Вы можете использовать его для автоматически отсортированного набора вещей, если точный порядок не имеет значения (то есть, что один конкретный 1.0 находится впереди или позади или точно на том же месте, что и другой 1.0).Даже тогда я бы использовал мультикарту / мультимножество, так как полагаться на коллизии или их отсутствие - не то, на что я бы рассчитывал.
Рассуждать о точном значении значений с плавающей запятой IEEE сложно, а хрупкостькода, на который он опирается, распространено.