Проблема, как вы и предполагали в своем вопросе, заключается в том, что вы сравниваете число с плавающей точкой с двойным.
Существует более общая проблема сравнения чисел с плавающей запятой, это происходит потому, что при выполнении вычисления числа с плавающей запятой результат вычисления может не соответствовать ожидаемому. Довольно часто последний бит полученного числа с плавающей запятой будет неправильным (хотя погрешность может быть больше, чем просто последний бит). Если вы используете ==
для сравнения двух значений с плавающей запятой, то все биты должны быть одинаковыми, чтобы значения с плавающей запятой были одинаковыми. Если ваши вычисления дают немного неточный результат, они не будут сравниваться, если вы ожидаете их. Вместо того, чтобы сравнивать значения, подобные этим, вы можете сравнить их, чтобы убедиться, что они почти равны. Для этого вы можете взять положительную разницу между числами с плавающей запятой и посмотреть, меньше ли она заданного значения (называемого эпсилоном).
Чтобы выбрать хороший эпсилон, нужно немного разбираться в числах с плавающей запятой. Числа с плавающей точкой работают аналогично представлению числа для данного числа значащих цифр. Если мы вычислим до 5 значащих цифр, и ваши вычисления приведут к тому, что последняя цифра результата будет неправильной, тогда 1.2345 будет иметь ошибку + -0.0001, тогда как 1234500 будет иметь ошибку + -100. Если вы всегда основываете свою погрешность на значении 1,2345, тогда ваша процедура сравнения будет идентична ==
для всех значений, больших 10 (при использовании десятичного числа). Это хуже в двоичном коде, все значения больше 2. Это означает, что выбранный эпсилон должен соответствовать размеру сравниваемых поплавков.
FLT_EPSILON - это промежуток между 1 и следующим ближайшим числом с плавающей точкой. Это означает, что может быть хорошим эпсилоном выбрать, будет ли ваше число между 1 и 2, но если ваше значение больше 2, использование этого эпсилона бессмысленно, потому что разрыв между 2 и следующим ближайшим числом с плавающей точкой больше, чем эпсилон. Поэтому мы должны выбрать эпсилон относительно размера наших поплавков (так как ошибка в расчете относительно размера наших поплавков).
Хорошая (ish) процедура сравнения с плавающей запятой выглядит примерно так:
bool compareNearlyEqual (float a, float b, unsigned epsilonMultiplier)
{
float epsilon;
/* May as well do the easy check first. */
if (a == b)
return true;
if (a > b) {
epsilon = scalbnf(1.0f, ilogb(a)) * FLT_EPSILON * epsilonMultiplier;
} else {
epsilon = scalbnf(1.0, ilogb(b)) * FLT_EPSILON * epsilonMultiplier;
}
return fabs (a - b) <= epsilon;
}
Эта процедура сравнения сравнивает числа с плавающей запятой относительно размера самого большого переданного числа с плавающей точкой. scalbnf(1.0f, ilogb(a)) * FLT_EPSILON
находит разрыв между a
и следующим ближайшим числом с плавающей точкой. Затем это умножается на epsilonMultiplier
, поэтому размер разницы можно регулировать в зависимости от того, насколько неточным будет результат вычисления.
Вы можете сделать простую процедуру compareLessThan
, например:
bool compareLessThan (float a, float b, unsigned epsilonMultiplier)
{
if (compareNearlyEqual (a, b, epsilonMultiplier)
return false;
return a < b;
}
Вы также можете написать очень похожую функцию compareGreaterThan
.
Стоит отметить, что сравнение поплавков не всегда может быть тем, что вы хотите. Например, это никогда не обнаружит, что значение с плавающей точкой близко к 0, если оно не равно 0. Чтобы исправить это, вам нужно решить, какое значение вы считаете близким к нулю, и написать для этого дополнительный тест.
Иногда неточности, которые вы получаете, не будут зависеть от размера результата вычисления, но будут зависеть от значений, которые вы вводите в расчет. Например, sin(1.0f + (float)(200 * M_PI))
даст гораздо менее точный результат, чем sin(1.0f)
(результаты должны быть идентичны). В этом случае ваша процедура сравнения должна будет взглянуть на число, введенное вами в расчет, чтобы узнать предел погрешности ответа.