2013-07-15 2 views
16

Правильное тестирование двух чисел с плавающей запятой для равенства - это то, что многие люди, включая меня, не полностью понимают. Сегодня, однако, я подумал о том, как некоторые стандартные контейнеры определяют равенство в терминах operator<. Я всегда вижу людей с проблемами, связанными с равенством, но никогда с другими реляционными сравнениями. Есть даже silent versions из них, которые включают в себя все, кроме равенства и неравенства.Если оператор <работает правильно для типов с плавающей точкой, почему мы не можем использовать его для тестирования равенства?

Предполагая operator< работы «правильно», в отличие от operator==, почему мы не могли сделать это:

bool floateq(float a, float b) { 
    //check NaN 
    return !(a < b) && !(b < a); 
} 

В самом деле, я запустить тест с дополнительной перегрузкой для двойников, как видно here, и кажется, имеют те же подводные камни, как сравнивая их с operator==:

std::cout << "float->double vs double: " 
      << floateq(static_cast<double>(0.7f), 0.7) << " " 
      << (static_cast<double>(0.7f) == 0.7) << "\n"; 

Выход:

float-> двойного против двойного: 0 0

Может ли я беспокоиться об использовании всех операторы сравнения, или есть какой-то другой аспект сравнения чисел с плавающей точкой, что я не понимающими правильно?

+3

Нет ничего плохого в равенстве, сравнивающем значения с плавающей запятой как таковые. Проблема в том, что результат может не соответствовать ожиданиям. –

ответ

14

==, <, >, <=, >= и != операторы работают очень хорошо с числами с плавающей точкой.

Вы, кажется, есть предпосылки, что некоторые разумное осуществление <должен сравнить (двойной) 0.7f равным 0,7. Это не тот случай.Если вы нанесли 0.7f на номер double, вы получите 0x1.666666p-1. Однако 0.7 равен 0x1.6666666666666p-1. Они не являются численно равными; на самом деле, (double)0.7f значительно меньше, чем 0.7 --- Было бы смешно сравнивать их равными.

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

+0

Я думаю, что это именно то, что мне нужно. Он не нажимал кнопку, при которой «float» оставляет дополнительную часть точности, отличную от «double», которая была изначально. Таким образом, человек должен быть меньше другого и, следовательно, не равен. Это тоже такой позор, потому что это облегчило бы мою жизнь, если бы это сработало: p – chris

+0

Отличный вопрос, а также отличный ответ. Для тех, кто пишет программное обеспечение, посвященное деньгам, я использую сравнение 'if (abs (a-b) <0.001)'. Как правило, финансовые системы идут только на половину или максимум на одну десятую. Определенно важно тщательно прояснить ситуацию. –

+0

Очень сложно делать финансовые расчеты с числами с плавающей запятой. Обычно это делается либо с помощью _integer_ количества копеек, либо с представлением _decimal_. В любом случае вам все равно нужно проявлять большую осторожность. –

1

Следующий код (который я изменил, так что компилирует: В частности, призыв к floateq был изменен на floatcmp) печатает float->double vs double: 1 0, не 0 0 (как можно было бы ожидать при сравнении этих двух значений как поплавки).

#include <iostream> 

bool floatcmp(float a, float b) { 
    //check NaN 
    return !(a < b) && !(b < a); 
} 

int main() 
{ 
    std::cout << "float->double vs double: " 
       << floatcmp(static_cast<double>(0.7f), 0.7) << " " 
       << (static_cast<double>(0.7f) == 0.7) << "\n"; 
} 

Однако то, что имеет значение для стандартной библиотеки является то, что operator< определяет строгий слабый порядок, который он на самом деле делает для плавающих типов.

Проблема с равенством заключается в том, что два значения могут выглядеть одинаково при округлении, чтобы сказать 4 или 6 мест, но на самом деле совершенно разные и сравниваются как не равные.

+0

Извините, образец кода, который я опубликовал, в основном был получен из моего теста, к которому было связано. У этого есть перегрузка для двух поплавков и перегрузка для двух удвоений. Я затронул вопрос, чтобы лучше отразить это и сохранить название согласованным. – chris

+0

Что касается фактического ответа, то не должно быть такого путаницы, сколько бы десятичных разрядов не было приспособлено для 'operator <'? – chris

+0

'operator <' не является строгим слабым порядком для типов с плавающей точкой, хотя, потому что '! (1 tmyklebu

-2

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

Другой пример кода, который показывает, что ваше сравнение не работает (http://ideone.com/mI4S76).

#include <iostream> 

bool floatcmp(float a, float b) { 
    //check NaN 
    return !(a < b) && !(b < a); 
} 

int main() { 
    using namespace std; 

    float a = 0.1; 
    float b = 0.1; 

    // Introducing rounding error: 
    b += 1; 
    // Just to be sure change is not inlined 
    cout << "B after increase = " << b << endl; 
    b -= 1; 
    cout << "B after decrease = " << b << endl; 

    cout << "A " << (floatcmp(a, b) ? "equals" : "is not equal to") << "B" << endl; 
} 

Выход:

B after increase = 1.1 
B after decrease = 0.1 
A is not equal toB 
+0

Где хранится случайная часть предыстории поплавка? Я вижу только бит знака, показатель степени и значимость. – tmyklebu

+0

Ну, я должен согласиться с тем, что формулировка, которую я использовал, отчасти расстраивает. Обновлено. – biocomp

+2

Проблема с вашим обновленным ответом заключается в том, что вы должны каким-то образом выбрать эту терпимость.Это не может быть слишком большим, или вы ошибочно скажете, что две разные вещи равны, и она не может быть слишком маленькой, или вы ошибочно скажете, что две «одинаковые» вещи разные. Выбор толерантности, когда это вообще можно сделать, обычно требует тщательного анализа ввода и округления, возникающих в результате вычислений, которые вы делаете на этом входе, и в этом случае вам, вероятно, очевидно, что вам нужно будет сравнить в пределах толерантность. – tmyklebu

1

Float и double оба находятся в двоичном эквиваленте научной нотации с фиксированным количеством значимых бит. Если результат бесконечной точности вычисления не является точно представимым, фактический результат является ближайшим, который точно представляется.

Есть два больших подводных камни с этим.

  1. Многие простые, короткие десятичные разложения, такие как 0,1, не являются точно представляемыми в поплавке или двойном.
  2. Два результата, которые были бы равны в арифметике с вещественным числом, могут отличаться в арифметике с плавающей запятой. Так, например, арифметику с плавающей точкой не ассоциативно - (a + b) + c не обязательно совпадает с a + (b + c)

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

Если такого допуска нет, это означает, что вы используете неправильный тип с плавающей запятой или не должны использовать с плавающей точкой вообще. 32-разрядный IEEE 754 имеет такую ​​ограниченную точность, что может оказаться очень сложным найти подходящий допуск. Как правило, 64-битный вариант намного лучше.

2

При использовании чисел с плавающей запятой реляционные операторы имеют значения, но их значения не обязательно совпадают с тем, как ведут себя реальные числа.

Если значения с плавающей точкой используются для представления реальных чисел (их нормального назначения), операторы, как правило, ведут себя следующим образом:

  • x > y и x >= y как следует, что числовое количество, которое x предполагается представляют, вероятно, больше, чем y, и в худшем случае, вероятно, не намного меньше y.

  • x < y и x <= y как следует, что числовое количество, которое x должен представлять, вероятно, меньше, чем y, и в худшем случае, вероятно, не намного больше, чем y.

  • x == y означает, что числовые величины, которые x и y представляют неотличимы друг от друга

Обратите внимание, что, если x имеет тип float и y имеет тип double, вышеуказанные значения будут достигнуты если аргумент double отличен до float. Тем не менее, при отсутствии конкретного приведения, C и C++ (а также многие другие языки) преобразуют операнд float в double перед выполнением сравнения. Такое преобразование значительно уменьшит вероятность того, что операнды будут сообщены «неразличимыми», но значительно увеличит вероятность того, что сравнение даст результат, противоположный тому, что на самом деле указывают предполагаемые цифры. Рассмотрим, например,

float f = 16777217; 
double d = 16777216.5; 

Если оба операнда приводятся к float, сравнение покажет, что значения неразличимы. Если их отличить до double, сравнение будет указывать на то, что d больше, даже если значение f должно представлять немного больше. В качестве более крайнего примера:

float f = 1E20f; 
float f2 = f*f; 
double d = 1E150; 
double d2 = d*d; 

Float f2 содержит лучшие float представления 1E40. Double d2 содержит лучшее изображение double 1E400. Числовая величина, представленная d2 is hundreds of orders of magnitude greater than that represented by f2 , but (double) f2> d2 . By contrast, converting both operands to float would yield f2 == (float) d2`, правильно сообщив, что значения несовместимы.

PS - Мне хорошо известно, что стандарты IEEE требуют, чтобы вычисления выполнялись так, как если бы значения с плавающей запятой представляли точную долю мощности двух фракций, но мало кто видел код float f2 = f1/10.0; как «установить f2 на представимую мощность 2-й фракции, которая ближе всего к одной десятой единицы в f1 ". Цель кода - сделать f2 десятой частью f1. Из-за неточности код не может полностью выполнить эту цель, но в большинстве случаев более полезно рассматривать числа с плавающей запятой как представляющие фактические числовые величины, чем рассматривать их как долю мощности двух.

Смежные вопросы