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

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

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

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

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

Фактически, я выполнил тест с дополнительной перегрузкой для парных, как показано здесь, и, похоже, это те же проблемы, что и сравнивая их с operator==:

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

Вывод:

float- > double vs double: 0 0

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

4b9b3361

Ответ 1

Операторы ==, <, >, <=, >= и != отлично работают с числами с плавающей запятой.

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

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

Ответ 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 и С++ (а также многие другие языки) преобразуют операнд 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. Двойной 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 в представимый мощность-две фракции, которая ближе всего к одной десятой единицы в f1". Цель кода - сделать f2 десятой частью f1. Из-за неточности код не может полностью выполнить эту цель, но в большинстве случаев более полезно рассматривать числа с плавающей запятой как представляющие фактические числовые величины, чем рассматривать их как долю мощности двух.

Ответ 3

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

#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 мест, но на самом деле совершенно разные и сравниваются как не равные.

Ответ 4

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

Есть две большие проблемы с этим.

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

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

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

Ответ 5

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

Другой пример кода, который показывает, что ваше сравнение не работает (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